链表的基础操做咱们都很熟悉,与此同时更流畅,完整,正确的解决,也是必不可少的基本功。所以我对它们作了一个总结,千万不要小看这些基础操做,熟悉它们之后你解决链表的问题将会驾轻就熟,那么,开始吧!node
先给出链表原型,后续的全部过程都基于这个结构体。算法
struct ListNode {
int val;
struct ListNode *next;
};
复制代码
四种基本操做:安全
建立,插入,删除,反转less
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。
插入的关键在于找到待插入位置以前的那个节点,改变它的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");
}
复制代码
其实删除或许彻底能够和插入放在一块儿,它的重点仍然是找到它的前一个节点,若是删除的是第一个元素,那么这是一种特殊状况,它的前面没有节点,应该单独考虑。
那么不把它们放在一块儿的缘由是什么呢?删除涉及到空间的回收,也就是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->nex
t,由于它已经被释放掉了,那咱们怎么推动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。
关于反转链表,我会很天然的想到头插法,那么的确这样,你彻底能够用头插法的视角来看待下面这个例子。
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去搜索挑战一下。