原文连接: https://subetter.com/algorith...
单链表常常为公司面试所说起,先不贬其过于简单,由于单链表确实是数据结构中最简单的一部分,但每每最简单的,人们越没法把握其细节。本文一共总结了单链表常被说起的各类操做,以下:html
本文中的全部操做均针对带有头结点的单链表。请注意:头结点和第一结点是两个结点,百度百科解释为:为方便操做,在单链表的第一个结点以前附设一个结点,称之为头结点。本文中用header代替头结点。node
在继续下文以前先约定下结点结构:c++
/* 定义结点结构 */ struct Node { int data; Node * next; Node() { data = 0; next = nullptr; } }; /* 定义头结点 */ Node * header = new Node;
例如:输入数据:1 2 3 4 5 6,构造单链表:6->5->4->3->2->1。面试
/* 逆序构造单链表,-1 结束输入 */ void desc_construct(Node * header) { Node * pre = nullptr; // 前一个结点 int x; while (cin >> x && x != -1) { Node * cur = new Node; cur->data = x; cur->next = pre; // 指向前一个结点 pre = cur; // 保存当前结点 } header->next = pre; // 头结点指向第一结点 }
例如:假设现有链表:6->5->4->3->2->1,进行反转操做后,链表变成:1->2->3->4->5->6。算法
/* 反转链表 */ void reverse(Node * header) { if (!header->next || !header->next->next) // 若是是空链表或链表只有一个结点 return; Node * cur = header->next; // 指向第一个结点 Node * pre = nullptr; while (cur) { Node * temp = cur->next; // 保存下一个结点 cur->next = pre; // 调整指向 pre = cur; // pre 前进一步 cur = temp; // cur 前进一步 } header->next = pre; // 头结点指向反转后的第一结点 }
咱们但愿用最小的时间复杂度来完成这个排序任务。归并排序是个不错的选择,平均时间复杂度$T(n)=O(nlogn)$,可是还有其余方法么?数据结构
咱们想到常常出现的快排,快排是须要一个指针指向头,一个指针指向尾,而后两个指针相向运动并按必定规律交换值,最后使得支点左边小于支点,支点右边大于支点,可是对于单链表而言,指向结尾的指针很好办,可是这个指针如何往前,咱们只有一个next(这并非一个双向链表)。oop
若是是这样的话,对于单链表咱们没有前驱指针,怎么能使得后面的那个指针往前移动呢?因此这种快排思路行不通,若是咱们能使两个指针都往next方向移动而且也能够按相同规律交换值那就行了,怎么作呢?this
接下来咱们使用快排的另外一种思路来解答。咱们只须要两个指针i和j,这两个指针均往next方向移动,移动的过程当中始终保持区间[1, i]的data都小于base(位置0是主元),区间[i+1, j)的data都大于等于base,那么当j走到末尾的时候便完成了一次支点的寻找。若以swap操做即if判断语句成立做为基本操做,其操做数和快速排序相同,故该方法的平均时间复杂度亦为$T(n)=O(nlogn)$。spa
/** * 链表升序排序 * * begin 链表的第一个结点,即 header->next * end 链表的最后一个结点的 next */ void asc_sort(Node * begin, Node * end) { if (begin == end || begin->next == end) // 链表为空或只有一个结点 return; int base = begin->data; // 设置主元 Node * i = begin; // i 左边的小于 base Node * j = begin->next; // i 和 j 中间的大于 base while (j != end) { if (j->data < base) { i = i->next; swap(i->data, j->data); } j = j->next; } swap(i->data, begin->data); // 交换主元和 i 的值 asc_sort(begin, i); // 递归左边 asc_sort(i->next, end); // 递归右边 } // how to use it? asc_sort(header->next, nullptr);
为简化问题,如下代码为合并两个升序链表。指针
/* 合并两个有序链表 */ void asc_merge(Node * header, Node * other_header) { asc_sort(header->next, nullptr); // 保证有序 asc_sort(other_header->next, nullptr); if (!header->next) // 链表为空 { header->next = other_header->next; // 合并后两个 header 指向第一个结点 return; } if (!list->header->next) // 链表为空 { other_header->next = header->next; // 合并后两个 header 指向第一个结点 return; } Node * p = nullptr; // 还需一个指针,指向合并的结点 Node * this_pointer = header->next; // 第一个结点 Node * other_pointer = other_header->next; // 第一个结点 // 单独考虑合并的第一个结点 if (this_pointer->data < other_pointer->data) { other_header->next = p = this_pointer; // p 指向新合并的结点 this_pointer = this_pointer->next; // 前进一步 } else { header->next = p = other_pointer; // p 指向新合并的结点 other_pointer = other_pointer->next; // 前进一步 } while (this_pointer && other_pointer) { if (this_pointer->data < other_pointer->data) { p->next = this_pointer; // 合并新结点 p = this_pointer; // p 前进一步指向新合并的结点 this_pointer = this_pointer->next; } else { p->next = other_pointer; // 合并新结点 p = other_pointer; // p 前进一步指向新合并的结点 other_pointer = other_pointer->next; } } // 处理剩下的结点 if (this_pointer) p->next = this_pointer; if (other_pointer) p->next = other_pointer; }
例如,给定链表1->4->3->5->6->8,返回倒数第3个数,也就是5。要求,只给定链表,但并不知道链表长度,如何在最短期内找出这个倒数第k个值。
其实思路很简单,假设k是小于等于链表长度,那么咱们能够设置两个指针p和q,这两个指针在链表里的距离就是k,那么后面那个指针走到链表末尾的nullptr时,另外一个指针确定指向链表倒数第k个值。
/* 返回链表倒数第k个值 */ int kth_last(Node * header, int k) { Node * p = header->next; Node * q = p; for (int i = 0; i < k; i++) { if (!q) { cout << "链表长度小于k\n"; return -1; } q = q->next; } while (q) { q = q->next; p = p->next; } return p->data; }
有环是什么意思?一个单链表最后一个结点的位置的next应该是nullptr,标志着链表的结尾,可是若是如今这个next指向了链表里的某一个结点(能够是自身),那么这个链表就存在环。以下图:
所以咱们只要找到两个结点,其地址相同(由于两个结点的data可能相同),便可判定有环。
咱们的思路就是:设置两个快慢指针(快慢指针即两个指针起点相同,慢指针每次走一步,快指针走两步),让它们一直往下走,直到它们相等,说明有环;遇到nullptr,说明无环。下面简单证实:如上图,A为链表第一个结点,B为环与链表的交叉点,C为slow_pointer
与fast_pointer
相遇的位置。假设环的长度为r,则有
$$ AB+BC+t_1r=\frac {AB+BC+t_2r}{2} \tag{左为慢指针,右为快指针} $$
化简为:
$$ AB+BC=(t_2-2t_1)r \tag{t1,t2为整数} $$
在肯定了AB和r后,只需调整BC,使AB+BC能整除r便可。
/* 判断链表是否有环,有环返回相遇结点 */ Node * is_loop(Node * header) { if (!header->next) // 空链表 return nullptr; Node * slow_pointer = header; Node * fast_pointer = header; while (fast_pointer->next && fast_pointer->next->next && slow_pointer != fast_pointer) { slow_pointer = slow_pointer->next; // 慢指针走一步 fast_pointer = fast_pointer->next->next; // 快指针走两步 } if (slow_pointer == fast_pointer) return slow_pointer; return nullptr; }
参考2.6图,若存在环且找到了相遇点C,此时令一个指针start_node从链表第一个结点处开始日后遍历,再令另外一个指针meet_node从C处日后遍历,它们的相遇结点就是环的入口点。为何呢?
2.6公式已经证实了:若快慢指针相遇在C点,则:
$$ AB+BC=tr \tag{t是整数} $$
进一步整理上式为:
$$ AB=(r-BC)+(t-1)r \tag{其中r-BC的含义请对照2.6图} $$
好了,至此,证实就已经很显然了。当start_node走了r-BC距离后,meet_node正好到达入口处B点,此时start_node还剩(t-1)r距离,显然两个指针继续走的话,必定会相遇在入口处B点。
/* 在一个有环链表中找到环的入口 */ Node * find_meet_node(Node * header) { Node * meet_node = is_loop(header); if (meet_node == nullptr) // 不存在环 return nullptr; Node * start_node = header->next; while (start_node != meet_node) { start_node = start_node->next; meet_node = meet_node->next; } return start_node; }
此外,咱们也会遇到“判断两个链表是否相交”,“求出两个相交链表的交点”这样的问题,百变不离其宗,咱们只需把链表尾接到其中一个链表头就转化为2.6和2.7的问题,因此在这里不做详述了。
题意规定,给定要删除的结点和头结点,现要你删除这个结点,要求平均时间复杂度为$T(n)=O(1)$。
例如,现有这样的链表,1->2->3->4->5->6,须要删除4,咱们的思路确定是先找到4的前一个结点3,和4的后一个结点5,而后把3和5连起来,再把4删除。可是这样作的话,咱们须要花费$O(n)$的时间来找到3和5,与题意要求的$O(1)$相距甚远。
咱们之因此须要从头结点开始查找要删除的结点,是由于咱们须要获得要删除结点的前一个结点。咱们试着换一种思路。若是咱们要删除4,能够把4和5的数据交换下,而后删除5,再把4和6链接起来,如此其时间复杂度为$O(1)$。
上面的思路还有一个问题:若是删除的结点位于链表的尾部,没有下一个结点,怎么办?咱们仍然从链表的头结点开始,顺便遍历获得给定结点的前序结点,并完成删除操做。这个时候时间复杂度是$O(n)$。那题目要求咱们须要在$O(1)$时间完成删除操做,咱们的算法是否是不符合要求?实际上,假设链表总共有n个结点,咱们的算法在n-1个状况下,时间复杂度是$O(1)$,只有当给定的结点处于链表末尾的时候,时间复杂度为$O(n)$。所以其平均时间复杂度$\frac {(n-1)⋅O(1)+1⋅O(n)}n$,仍然为$O(1)$。
/* 删除当前结点 */ void del(Node * header, Node * position) { if (!position->next) // 要删除的是最后一个结点 { Node * p = header; while (p->next != position) p = p->next; // 找到 position 的前一个结点 p->next = nullptr; delete position; } else { Node * p = position->next; swap(p->data, position->data); position->next = p->next; delete p; } }
题意要求,给定链表头结点,在最小复杂度下输出该链表的中间结点。
若是只知链表的头结点,咱们通常的思路就是先遍历链表获得链表长度,而后再遍历一遍获得中间结点,如此时间复杂度为$O(n)+O(\frac n2)$。
上面的思路彷佛不太使人满意。咱们又想到快慢指针,它有一个很重要的性质:慢指针走的长度等于快慢指针相距的程度。因此利用这个性质,当快指针走到链表尾时,慢指针正好在中间结点。
/* 找出单链表的中间结点 */ Node * find_middle(Node * header) { Node * slow_pointer = header; Node * fast_pointer = header; while (fast_pointer->next && fast_pointer->next->next) { slow_pointer = slow_pointer->next; // 慢指针走一步 fast_pointer = fast_pointer->next->next; // 快指针走两步 } return slow_pointer; }