链表操做基础

链表的基础操做咱们都很熟悉,与此同时更流畅,完整,正确的解决,也是必不可少的基本功。所以我对它们作了一个总结,千万不要小看这些基础操做,熟悉它们之后你解决链表的问题将会驾轻就熟,那么,开始吧!node

先给出链表原型,后续的全部过程都基于这个结构体。算法

struct ListNode {
    int val;
    struct ListNode *next;
};
复制代码

四种基本操做:安全

建立,插入,删除,反转less

  • Create
  • Insert
  • Remove
  • Reverse

1.Create

头插法

struct ListNode* createList(int n){
	struct ListNode* head = NULL;
	for (int i = 0; i < n; ++i)
	{
		struct ListNode* p = (struct ListNode*)malloc(sizeof(struct ListNode));
		//Q1
		if(!p)
			return NULL;
		//Q2
		p->val = i;
		p->next = head;
		head = p;
	}
	return head;
}
复制代码

要生成一个长度为n的链表,每一个循环里,new 一个p节点,让它的下一个节点指向链表头,迭代,将这个p节点更新为链表头,返回这个链表头函数

头插法的特色在于你插的最后一个节点链表头,意思是你顺序访问链表时将获得一个和插入序列相反的序列。测试

Q1:为何检查p?这样操做有风险吗?ui

由于动态内存分配存在失败的可能,当失败后返回的p是一个空指针,那么接下来两行对p的间接访问都将报错。this

即便这样处理,依然存在一个问题,那就是咱们是在某一次循环过程当中malloc失败,咱们让函数返回,意味着整个createList函数失败,而且咱们在前面成功分配的那部份内存并无回收,咱们甚至都没有一个手段访问到那些已经成功分配了的内存,那么这个函数就会像一个黑洞,每一次失败的调用,都会产生一部分空间的浪费,内存泄漏就极有可能会发生。所以,更周全的作法也许是在分配失败时,咱们在函数内部free掉以前已经的内存。spa

不过好在这种状况,在咱们写到的函数里基本不会发生,咱们能够放心使用上面的作法生成一个链表。指针

Q2:有其余的赋值方法吗?最好方便测试?

固然,咱们能够根据咱们对链表的操做对它进行修改。 例如咱们想对测试链表排序,咱们可让i换成rand。 咱们想要删除链表重复元素,咱们可让它等于i / 3,这样会产生连续三个相同值的节点。 咱们想产生1-2-3-4-5而不是5-4-3-2-1那可让它变成n-i

尾插法

struct ListNode* createList(int n){
	struct ListNode* head = (struct ListNode*)malloc(sizeof(struct ListNode));
	struct ListNode* tail = head;
	for (int i = 0; i < n; ++i)
	{
		struct ListNode* p = (struct ListNode*)malloc(sizeof(struct ListNode));
		if(!p)
			return NULL;
		p->val = i;
		p->next = NULL;
		tail->next = p;
		tail = p;
	}
    //Q1
	return head->next;
}
复制代码

与头插法相反,尾插法将每个新的p节点看成是链表的尾节点,它们的next域通通初始化为NULL,迭代的是tail,每一次插入后让tail指向新的p,对照着头插法,你很快会弄清楚尾插的原理。

Q1:为何返回的是head->next?

由于咱们第一次循环时,tail实际上同时也是head,这时的这第一个p应该就是咱们所但愿返回的链表头,head->next即是这一个p,因此我返回head->next。

2.Insert

插入的关键在于找到待插入位置以前的那个节点,改变它的next域,让它指向插入的新节点p。所以,特殊的位置每每发生在链表头,它以前没有节点了,是NULL。咱们须要对这种特殊状况考虑。固然,若是一个链表有一个val域无心义的头节点dummyHead,这个问题也就不存在了。下面的即是一个有头节点的例子,注意,有头节点的链表在访问时都要先往前推动一个。

咱们来看LeetCode上关于基础操做的一道题:707.Design Linked List.

题目描述过长我就不引用了,上面连接过去看吧。

个人解答:

MyLinkedList* myLinkedListCreate() {
    MyLinkedList* p = (MyLinkedList*)malloc(sizeof(MyLinkedList));
    p->val = -1;
    p->next = NULL;
    return p;
}

int myLinkedListGet(MyLinkedList* obj, int index) {
	if(index < 0)
		return -1;
	if(!obj->next)
		return -1;
	MyLinkedList* p = obj->next;
    for (int i = 0; i < index; ++i)
    {
    	p = p->next;
    	if(!p)
    		return -1;
    }
    return p->val;
}

void myLinkedListAddAtHead(MyLinkedList* obj, int val) {
	MyLinkedList* p = (MyLinkedList*)malloc(sizeof(MyLinkedList));
	if(!p)
		return;
	p->val = val;
	p->next = obj->next;
	obj->next = p;   
}

void myLinkedListAddAtTail(MyLinkedList* obj, int val) {
    MyLinkedList* p = obj;
    while(p->next){
    	p = p->next;
    }
    MyLinkedList* newp = (MyLinkedList*)malloc(sizeof(MyLinkedList));
    if(!newp)
		return;
    newp->val = val;
    newp->next = NULL;
    p->next = newp;
}

void myLinkedListAddAtIndex(MyLinkedList* obj, int index, int val) {
	if(index < 0)
		return;
	MyLinkedList* p = obj;
    for (int i = 0; i < index; ++i)
    {
    	p = p->next;
    	if(!p)
    		return;
    }
    MyLinkedList* newp = (MyLinkedList*)malloc(sizeof(MyLinkedList));
    if(!newp)
		return;
    newp->val = val;
    newp->next = p->next;
    p->next = newp;
}

void myLinkedListDeleteAtIndex(MyLinkedList* obj, int index) {
    if (index < 0)
    	return;
    MyLinkedList* tmp, *p = obj;
    if(!obj->next)
    	return;
    for (int i = 0; i < index; ++i)
    {
    	//ensure the delete node is not null
    	if(!p->next->next)
    		return;
    	p = p->next;
    }
    //p is before the node tobe deletes 
    //normal
    tmp = p->next;
    p->next = p->next->next;
    free(tmp);

}

void myLinkedListFree(MyLinkedList* obj) {
	MyLinkedList* tmp;
	while(obj){
		tmp = obj;
		obj = obj->next;
		free(tmp);
	}   
}

void myLinkedListPrint(MyLinkedList* obj){
	MyLinkedList* p = obj->next;
	while(p){
		printf("%d->", p->val);
		p = p->next;
	}
	printf("NULL\n");
}
复制代码

3.Remove

其实删除或许彻底能够和插入放在一块儿,它的重点仍然是找到它的前一个节点,若是删除的是第一个元素,那么这是一种特殊状况,它的前面没有节点,应该单独考虑。

那么不把它们放在一块儿的缘由是什么呢?删除涉及到空间的回收,也就是free的正确使用,这将是一个很容易出错的地方。关于free, 我会在最后再提到它。

例子能够参考上面代码里的myLinkedListDeleteAtIndex()

来作几道题吧!

203.Remove Linkedlist Elements
Problems
Remove all elements from a linked list of integers that have value val.

Example: Input: 1->2->6->3->4->5->6, val = 6 Output: 1->2->3->4->5

第一种作法,定义一个显示的pre指针,并保证它的指向始终指向删除元素的前一个位置,那么显然,对第一个元素的删除是一种特殊状况。

struct ListNode* removeElements_2(struct ListNode* head, int val) {
	struct ListNode* cur = head, *pre = NULL, *tmp;
	while(cur != NULL){
		if(cur->val == val){			
			if(pre != NULL)
				pre->next = cur->next;			
			else
				head = head->next;	
			tmp = cur;						
			cur = cur->next;
			free(tmp);	
		} else{
			pre = cur;
			cur = cur->next;
		}		
	}	
	return head;   
}
复制代码

对于这种作法,我在第7行对这种状况进行了处理。那就是改变head的指向而不是改变pre->next,由于你没法对一个NULL指针进行->运算,由于他实际上包含了指针的间接访问,pre->next = (*pre).next,这一点你在使用->它的时候应该很清楚才是。

关于空间释放,这里要释放的固然是cur节点,由于要删除的就是它,那么为何不直接free(cur)呢?free(cur)以后,咱们没法访问cur->next,由于它已经被释放掉了,那咱们怎么推动cur呢?答案是设置一个临时指针,先将以前的cur节点记录下来,咱们在访问事后,释放那个tmp节点,就很是安全了。

另外一种作法能够避免删除头元素的特殊状况,它使用到了指针的指针&运算符,取地址运算符也算是C的一大特性了,同时,这个例子也是指针的指针的经典运用

struct ListNode* removeElements(struct ListNode* head, int val) {
	struct ListNode ** p = &head, * tmp;
	while(*p){
		if((*p)->val == val){
			tmp = *p;
			*p = (*p)->next;
			free(tmp);
		}
		else
			p = &((*p)->next);
	}
	return head;   
}
复制代码

该算法中用来推动的是*p,咱们发如今删除时咱们改变了*p,这实际上操纵的就是当前元素,意思是咱们的前一个元素无需作出改变,依然指向它原先指向的节点,而咱们对这个节点直接作出改变,让它变成它的下一个节点,再释放掉它,删除就完成了。那么为何它能够避免删除头元素的特殊状况呢?由于咱们根本就不用考虑前一个节点了。

要改变一个指针p的内容咱们经过*p实现,那么要改变一个指针p,咱们须要使用*pp,其中pp = &p,pp实际上就是一个指针的指针。

那么像p1 = p2这样又表明着什么呢?为了弄清楚上面的内容,再举一个例子。

int a = 3, b = 2;
a = b;
b = 4;
复制代码

如今a = ?显然 a = 2。 只是把b当前的值赋给了a。p1 = p2一样也只是让p2当前的值赋给了p2,p1和p2之间在这行语句后就没什么联系了。

再来一道, 83.Remove Duplicates from Sorted List II

Given a sorted linked list, delete all nodes that have duplicate numbers, leaving only distinct numbers from the original list.

Example 1:
Input: 1->2->3->3->4->4->5 Output: 1->2->5

Example 2:
Input: 1->1->1->2->3 Output: 2->3

个人解答:

struct ListNode* deleteDuplicates(struct ListNode* head) {
    if(!head || !head->next)
        return head;
    struct ListNode* fakeNode = (struct ListNode*)malloc(sizeof(struct ListNode));
    fakeNode->next = head;
    struct ListNode* pre = fakeNode, *tmp;
    //pre是第一个uinique元素出现的前一个节点
    while(head){
        //释放了这些节点的内存,推动了head,但没有改变指向
        while(head->next && head->val == head->next->val){
            tmp = head;
            head = head->next;
            free(tmp);
        }
        //说明head是unique的,直接往前推动
        if(pre->next == head){
            pre = pre->next;
            head = head->next;
        }
        //一次性改变pre->next,pre不推动
        else{
            pre->next = head->next;
            free(head);
            head = pre->next;
        }
    }
    return fakeNode->next;    
}
复制代码

这一样是一个增长头节点避免特殊状况的例子,fakeNode就是这个头节点。
须要注意的是,我采起的策略里并非每删除一个元素就改变它前一个节点的指向,而是在推动到不等于当前连续的这些值时,一次性改变pre->next让它直接指向下一个,也就是值开始不一样的那个节点。

关于删除,leetcode上还有这些题能够参考:237,83,707,82。

4.Reverse

关于反转链表,我会很天然的想到头插法,那么的确这样,你彻底能够用头插法的视角来看待下面这个例子。

206. Reverse Linked List

Reverse a singly linked list.

Example:

Input: 1->2->3->4->5->NULL Output: 5->4->3->2->1->NULL

struct ListNode* reverseList(struct ListNode* head) {
    struct ListNode* tmpHead = NULL;
    struct ListNode* p = head;
    while(p != NULL){
    	struct ListNode* tmp = p->next;
    	p->next = tmpHead;
    	tmpHead = p;
    	p = tmp;
    }
    return tmpHead;
}
复制代码

很简单,对不对,就是遍历原链表,再将节点依次用头插法的方式插入。值得注意的是,如今链表的结构已经改变,咱们作的是in-place,也就是在原有空间上作出的修改,咱们如今的head已经变成了什么?

事实上,观察一个链表的结构有没有被破坏,咱们只用看对链表的访问过程当中咱们有没有对p->next有没有改变,即便p不是head自己,在本例中咱们第一次循环时便改变了它,这个时候head->next变成了NULL,对,它变成了尾节点。改变p对原链表的结构其实是没有影响的。想一想为何?

再看一例:
25. Reverse Nodes in k-Group

Given a linked list, reverse the nodes of a linked list k at a time and return its modified list. k is a positive integer and is less than or equal to the length of the linked list. If the number of nodes is not a multiple of k then left-out nodes in the end should remain as it is.

Example: Given this linked list: 1->2->3->4->5 For k = 2, you should return: 2->1->4->3->5 For k = 3, you should return: 3->2->1->4->5

思路:一次选择k个元素一组,这一组中第一个元素设为first,而后将这个组反转kHead是这个反转子串的头,设立一个pre指针,始终指向反转后最后一个元素,开始时pre指向一个dummyHead,也就是这里的head2。每次循环结束前让pre->next指向新的kHead

个人解答:

struct ListNode* reverseKGroup(struct ListNode* head, int k) {
	int length = 0; 
	struct ListNode* p = head;
	while(p){
		p = p->next;
		length += 1;
	}	
	if(length < k)
		return head;
	int res_node = length;
	struct ListNode* head2 = (struct ListNode*)malloc(sizeof(struct ListNode)); 
	struct ListNode* pre = head2;
	struct ListNode* kHead = NULL, *tmp, *first = NULL;
	while(res_node >= k){
		int n = k;
		kHead = NULL;
		first = NULL;
		while(n--){
			first = first?first:head;
			tmp = head->next;
			head->next = kHead;
			kHead = head;
			head = tmp;
		}
		pre->next = kHead;
		pre = first;
		res_node -= k;
	}
	pre->next = head;
	return head2->next;
}
复制代码

dummyHead真香~

更多关于reverse的还有成对交换啊,指定位置反转啊,利用反转花式改变链表结构啊,利用反转判断回文啊,leetcode上都有对应的题目,能够按tag去搜索挑战一下。

相关文章
相关标签/搜索