链表题目总结(第一篇)

首先感谢下面几个博客的技术支持:

  http://www.cnblogs.com/newth/archive/2012/05/03/2479903.html 

另外,这里再贴上一些我的好朋友的关于链表的总结贴子

  http://www.cnblogs.com/carlsama/p/4123709.html

  http://www.cnblogs.com/carlsama/p/4126470.html

  http://www.cnblogs.com/carlsama/p/4126503.html

  http://www.cnblogs.com/carlsama/p/4127201.html

这里分析的链表默认情况下都是单链表,本篇主要分析一条单链表上的各种问题与解答

(一)单链表数据结构  

1 struct ListNode{
2     int val;
3     ListNode *next;
4     ListNode(int val = 0) : val(val), next(NULL){}
5 };

(二)单链表相关题目

  (1)倒序打印

  以倒序的方式访问节点,比如打印或者其他操作.

1 // 方法 i:   递归的打印, 代码非常的简洁
2 void ReversePrint(ListNode *pHead){
3     if(!pHead)  return;
4     ReversePrint(pHead->next);
5     std::cout<<pHead->val<<std::endl;
6 }
 1 // 方法 ii:  使用栈,仿照递归的遍历
 2 void ReversePrint(ListNode *pHead){
 3     if(!pHead)  return;
 4     stack<ListNode*>    data;
 5     ListNode *pos = pHead;
 6     while(pos){
 7         data.push(pos);
 8         pos = pos->next;
 9     }   
10     ListNode *cur;
11     while(!data.empty()){
12         cur = data.top();
13         std::cout<<cur->val<<std::endl;
14         data.pop();
15     }   
16 }

  (2)获取链表倒数第K个节点

 1 // 设置两个指针,两个指针之间距离保持为k,前面的指针从头节点开始, 然后一起往后面走
 2 ListNode * RGetKthNode(ListNode *pHead, unsigned int k){ 
 3     if(k < 1)   return NULL;
 4     ListNode *pre = pHead;
 5     ListNode *last = pHead;
 6     while(k-- > 0 && last){
 7         last = last->next;
 8     }   
 9     if(!last) return NULL;
10     while(last){
11         last = last->next;
12         pre = pre->next;
13     }   
14     return pre; 
15 }

  (3)查找链表的中间节点

 1 // 慢指针以每步一个节点的速度前进,快指针以每步两个节点的速度前进,这样快指针到达末端时候,慢指针就指向中间节点
 2 ListNode* getMidNode(ListNode *pHead){
 3     if(!pHead || !pHead->next)  return pHead;
 4     ListNode *slow = pHead, *fast = pHead;
 5     while(fast && fast->next){
 6         slow = slow->next;
 7         fast = fast->next;
 8         fast = fast->next;
 9     }   
10     return slow;
11 }

  (4)链表反转

//方法i    new一个头节点,然后遍历链表,不断的插入到头节点的next位置
ListNode *ReverseList(ListNode *pHead){
    if(!pHead || !pHead->next)  return pHead;
    ListNode * newhead = new ListNode();
    ListNode *pos = pHead, *tmp;
    newhead->next = pHead;
    while(pos->next){
        tmp = pos->next;
        pos->next = tmp->next;
        tmp->next = newhead->next;
        newhead->next = tmp;
    }
    return newhead->next;
}
 1 //方法ii 就地倒转 + 迭代, 反转之前pre->cur, 反转之后 cur->pre
 2 ListNode *ReverseList(ListNode *pHead){
 3     if(!pHead || !pHead->next)  return pHead;
 4     ListNode *pre = pHead, *cur = pHead->next, *next = NULL;
 5     pHead->next = NULL;
 6     while(cur){
 7         next = cur->next;
 8         cur->next = pre;
 9         pre = cur;
10         cur = next;
11     }   
12     return pre;
13 }
1 //方法iii 就地倒转 + 递归,     非常具有技巧性
2 ListNode *ReverseList(ListNode *pHead){
3     if(!pHead || !pHead->next)  return pHead;
4     ListNode *left = ReverseList(pHead->next); // 返回left为left段倒转后的首节点
5     pHead->next->next = pHead; // pHead->next 一开始是left段的首节点,倒转后就是末节点
6     pHead->next = NULL;
7     return left;
8 }

  (5)链表节点的删除

  给定链表的头节点指针和要删除的节点指针,一般来说通常想到的方法就是从头节点开始遍历,找到删除节点的pre节点,借助pre节点删除该节点,这个时间复杂度是O(n),这种方法实在不允许直接数值交换的情况下才只能这样,如果允许节点之间的数值交换或者拷贝,那么有平均时间为O(1)的方法:

 1 // delete Node, with O(1), 平均时间复杂度
 2 void deleteNode(ListNode *pHead, ListNode *pDelete){
 3     if(!pHead || !pDelete)  return;
 4     if(pDelete->next){
 5         // 不是最后一个节点,只需要复制下一个节点的值过来然后删除下一个节点, O(1)
 6         ListNode *tmp = pDelete->next;
 7         pDelete->val = pDelete->next->val;
 8         pDelete->next = pDelete->next->next;
 9         delete tmp;  // 很容易忘记
10     }else{
11         // 是最后一个节点,只能从前遍历到倒数第二个节点,O(n)
12         if(pHead == pDelete){ // 只有一个节点是个特殊情况
13             delete pHead;
14             pHead = NULL;
15         }
16         ListNode *pos = pHead;
17         while(pos->next != pDelete){
18             pos = pos->next;
19         }
20         pos->next = NULL;
21         delete pDelete;   // 这一句很容易忘记
22     }   
23 }

  (6)在链表指定节点前插入某个节点

  对于单链表,我们知道插入指定节点后面是很容易的,但是要插入到指定节点前面似乎需要从头遍历链表,如果允许节点之间值拷贝的话那么就可以先插入到后面

然后两个节点交换一下值便变相实现插入到前面

1 // 题目要求在指定节点pPos前插入,我们可以先插入到后面,然后交换他们的值即可
2 void insertNode(ListNode *pPos, ListNode *pInsert){
3     if(!pPos || !pInsert)   return;
4     pInsert->next = pPos->next;
5     pPos->next = pInsert;
6     int tmp = pPos->val;
7     pPos->val = pInsert->val;
8     pInsert->val = tmp;
9 }

  (7)链表是否有环

  

// 快慢指针, 慢指针每步一个节点,快指针每步两个节点,如果有环肯定会相遇
bool isCircleList(ListNode *pHead){
    if(!pHead || !pHead->next) return false;
    ListNode *slow = pHead, *fast = pHead;
    while(fast && fast->next){
        slow = slow->next;
        fast = fast->next->next;
        if(slow == fast)    return true;
    }   
    return false;
}

  这里解释一下:       假设链表里面有环,那么快指针会先进入环内,然后在里面不断的循环,当慢指针进入环内那一刻,可以根据相对运动的观念,可以看成慢指针静止,快指针以每步一节点的速度走,那么很显然必然会相遇

  (8)判断有环链表的环入口点

   先使用一快一慢指针,快指针一步两个节点,慢指针一步一个节点,一是用来探测链表是否有环,二是如果有环那么就已经把快指针送到了环内部。

这样之后,慢指针重置为指向链表头节点,快指针指向不变,但是移动速度变为每步一个节点,即和慢指针速度一样,然后快慢指针同步移动,那么相遇的时候

就是环的入口点。

//
ListNode *GetFirstCircleNode(ListNode* pHead){
    if(!pHead || !pHead->next)  return pHead;
    ListNode *slow = pHead, *fast = pHead;
    // fast指针先进入环内  第一部分代码
    while(fast && fast->next){
        slow = slow->next;
        fast = fast->next->next;
        if(slow == fast)
            break;
    }   
    if(!fast || !fast->next)    return NULL;
    // slow指针再次从头开始, fast指针减速
    slow = pHead; // 第二部分代码
    while(slow != fast){
        slow = slow->next;
        fast = fast->next;
    }   
    return slow; // 相遇点就是环的入口
}

   这里面代码似乎很简单,但是要证明其正确性需要费一点周折:

    

  如上图所示: 假设链表的头节点在s处, O为链表入口点, m为第一部分代码中快慢指针的相遇点,

以O点为坐标原点,以向右为正方向,以相隔链表节点数目为坐标的值,那么m点坐标为m(0, p), p >= 0  

p 为m点到o点中间的节点个数,同理s点坐标为(0, -d), d >= 0; 另外设环的大小为r, 即有r个节点

  那么在第一部分代码中, 两个节点在m点相遇时候,慢指针移动距离 d + p,  快指针为2d + 2p;

那么 2d + 2p - d - p =  d + p ,因为快指针先进入圈内,然后再和慢指针相遇时候必然比慢指针多走n圈,

n >= 1, 所以  

        d + p = n * r      (*   结论1)

  然后在第二部分代码中,快慢指针一起同步的走, 我们可以看到当慢指针走动距离为d的时候,快指针这时候

和慢指针同速,所以走动距离也是d:

  那么, 此时慢指针的坐标是 -d + d = 0, 即O(0,0)点,而快指针的坐标是 d + p, 即(0,d+p),而根据结论1

我们可以看到 d+ p = n*r , n >= 1, 所以点(0,d+p)就是O(0,0),此时两个指针相遇,而之前慢指针一直都未进入到环,快

指针则一直在环内,所以这一次相遇是它们的第一次相遇点,同样也是环的入口点 

  (9)链表的排序

  这里链表的排序主要都是基于归并排序,不过可以分为迭代的归并排序和递归的归并排序

 1 // 递归的方式实现归并排序
 2 ListNode *ListSort(ListNode *pHead){
 3     if(!pHead || !pHead->next)  return pHead;
 4     ListNode *mid = getMidNode(pHead);
 5     ListNode *right = pHead, *left = mid->next;
 6     mid->next = NULL;
 7     right = ListSort(right);
 8     left = ListSort(left);
 9     pHead = MergeSortedList(right, left);
10     return pHead;
11 }
 1 //仿照SGI STL里面的List容器的sort函数,实现迭代版的归并排序
 2 ListNode *ListSort(ListNode *pHead){
 3     if(!pHead || !pHead->next)  return pHead;
 4     vector<ListNode*> counter(64, NULL);
 5     ListNode *carry;
 6     ListNode *pos = pHead;
 7     int fill = 0;
 8     while(pos){
 9         carry = new ListNode(pos->val);
10         pos = pos->next;
11         int i = 0;
12         for(i = 0; i < fill && counter[i]; i++){
13             carry = MergeSortedList(carry, counter[i]); // 合并两个已排序的链表,参见链表总结第二篇
14             counter[i] = NULL;
15         }
16         counter[i] = carry;
17         if(i == fill) fill++;
18     }
19     for(int i = 1; i < fill; i++){
20         counter[i] = MergeSortedList(counter[i-1], counter[i]);
21     }
22     return counter[fill-1];
23 }

   下面以链表数据4,2,1,5,6,9,7,8,10为例分析这个迭代版的代码过程

  在while循环里面每次都是从原链表里取出一个节点,然后往counter数组里面归并,fill值表明目前counter数组中

存有数据的最大的那个数组标号+1,比如说,我们首先取出4,此时fill = 0, 直接就把4放在counter[0]链表上,fill变为1,

然后取出2,就拿2与counter[0]进行merge,得到结果放到count[1],fill变为2,此时counter数组的情况是counter[0]为空,

counter[1]存放2,4,再加入1时候直接放到counter[0], 再加入5时候,1与5merge,得到1,5再继续向上和couter[1]中

的2,4,merge得到1,2,4,5,如果此时counter[2]不为空,那么继续merge,这里为空则1,2,4,5存到counter[2],

于是就这样一步步的向上merge,每次merge链表长度都是加倍

  最终取完所有数据之后,counter数组里面的数据需要最终整合一下,代码中最后一个for循环就是不断的把couter中的内容

向上merge,最终形成最后的结果 

  这里其实也就是形成了一颗归并树,时间复杂度依然是O(nlgn)

  

原文地址:https://www.cnblogs.com/sosohu/p/4127213.html