单链表的数据结构非常简单,但是基于单链表的一些算法却特别有意思。这里对各个算法的思路、要点、java实现、测试做一个简单的总结,具体如下:
- 单链表的倒序输出
- 查到单链表倒数第k个元素
- 获取单链表的中间元素
- 两个有序单链表合并
- 判断两个单链表是否相交
- 获取两个单链表相交的结点
- 判断单链表是否有环
- 判断有环单链表中,环的长度
- 获取单链表中,环的起始结点
- 删除单链表中指定结点
完整代码(包括测试用例): (https://github.com/FergusChen/AlgoCode)
包路径:main.linklist
单链表的数据结构
首先,定义单链表的数据结构。
单链表的倒序输出
单链表的倒序输出即从链表表尾逐个输出到表头。因为单链表的指针域是单向的,所以要找到表尾的话,需要遍历整个链表。
这个算法可以有两种实现:1、迭代实现,2、递归实现。
迭代实现
迭代实现关键是考虑倒序输出正好符合栈的先进后出的特点——先遍历到的元素最后输出。这样,可以遍历一遍,将数据存储在栈中,然后由栈逐个pop即可。下面是用迭代实现代码:
递归实现
递归实现即递归地往下找,直到找到最后一个元素,再回溯输出即可。但是,递归实现有个很大的缺陷,就是当链表元素特别多的时候,递归层次太深,容易造成栈溢出。 。下面是递归实现代码:
查找单链表倒数第k个元素
查找单链表中倒数第k个元素,仍然有两种实现:1、通过获取链表的长度实现。2、通过两个指针实现。
通过获取链表的长度的实现
第1次遍历,得到链表长度 length。然后第2次遍历,找到第length-k个元素就是了。这种方法效率较低。
通过两个指针实现
如果不允许遍历整个链表,或者要提高效率,就采用这种思路:
- 定义两个指针,pFront先指向第k个元素,pBack指向链表的head
- 两个指针同时移动遍历,当pFront遍历到链表尾部之后,pBack正好是指向倒数第k个元素。12345678910111213141516171819202122232425/*** 查到单链表倒数第k个元素* test: (null, 2), (p1, 2), (p1, 200), (p1, 0)* @param head 待查找的结点* @param k 倒数第k个元素* @return Node 返回倒数第k个结点 , 若k=0或链表为null,则返回null,若k大于链表长度,则抛RuntimeException异常*/public static Node getReKthNode2(Node head, int k) {if (head == null || k == 0) return null;Node pFront = head;Node pBack = head;while (k > 1 && pFront != null) { //从1开始数第k个数。pFront = pFront.next;--k;}if (pFront == null) {throw new RuntimeException("exceed length of linklist");}while (pFront.next != null) {pBack = pBack.next;pFront = pFront.next;}return pBack;}
获取单链表的中间元素
思路:仍然是两个指针,只是pFront一次走2步,pBack一次走1步。直到pFront走到最后一个结点。
两个有序单链表合并
两个单链表head1和head2都各自有序,要求将其合并,得到的结果依然有序。。此问题仍然有两种实现:1、归并排序的思想合并; 2、递归合并
归并排序的思想合并
思想:用归并排序merge方法的思想。
递归合并
用递归的方法代码更简洁,也可读性也很强。但是,两个链表比较长的时候,递归层次也可能会很深,结果造成栈溢出。
判断两个单链表是否相交
如果两个链表相交于某一节点,那么在这个相交节点之后的所有节点都是两个链表所共有的。相当于“Y”字型的两个链表。
思路:只需要判断两个链表的最后一个节点是否相同即可。这样,只需要比较一次,但两个链表都遍历到尾部,时间复杂度为O(len1+len2)
获取两个单链表相交的结点
若两个链表相交,则获取相交处的结点。 即“Y”的交点
思路:如果有交点,则两个链表从交点到尾部一定是相同的元素,而且这段距离小于或等于最短的链表。
- 则可以定义两个指针pFront和pBack,pBack指向较短链表的首部,pFront指向较长链表的lenLong- lenShort位置
- 两个指针同时移动,直到找到相同结点。123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051/*** 获取两个单链表相交的结点* test:(null, null), (null, head2),(head1, null), (head1和head2相交), (head1和head2不相交)** @param head1 链表1* @param head2 链表2* @return Node 返回两个链表相交的结点。若没有相交,则返回null*/public static Node getCommonNode(Node head1, Node head2) {if (head1 == null || head2 == null) return null;int len1 = 1;int len2 = 1;Node tail1 = head1;Node tail2 = head2;while (tail1.next != null) {tail1 = tail1.next;++len1;}while (tail2.next != null) {tail2 = tail2.next;++len2;}if (tail1 != tail2) {return null;//不相交}Node pFront = null;Node pBack = null;int distance = 0;//确定head1和heda2哪个比较长,pBack指向短的链表表头if (len1 <= len2) {pBack = head1;pFront = head2;distance = len2 - len1;} else {pBack = head2;pFront = head1;distance = len1 - len2;}//让pFront指向lenLong-lenShort的位置for (int i = 0; i < distance; i++) {pFront = pFront.next;}//两个指针同时向链表尾部移动,直到找到交点。while (pBack != pFront) {pBack = pBack.next;pFront = pFront.next;}return pFront;}
判断单链表是否有环
如果链表有环,单用一个指针是永远无法遍历到尾部的。那么,判断是否有环的思路,仍然是两个指针。
思想:
- 定义两个指针都指向head
- 一个指针一次走两步,另一个指针一次走一步。
- 如果两个指针指到了同一个元素,证明有环。
该算法的时间复杂度是O(n)1234567891011121314151617181920/*** 判断单链表是否有环* test:(null), (偶数个元素、偶数环元素的链表), (偶数个元素且无环的链表),(奇数个元素、奇数环的链表),* (奇数个元素且无环的链表),(1个元素的无环链表),(1个元素有环链表),(只有2个元素且有环的链表),(圈链表)* @param head 待判断是否有环的链表* @return boolean 是否有环。false:无环。true:有环*/public static boolean hasCycle(Node head) {if (head == null || head.next == null) return false;Node p1 = head;Node p2 = head;while (p2 != null && p2.next != null) {p1 = p1.next;p2 = p2.next.next;//这一步需要考虑健壮性。即如果p2.next为null,则p2.next.next就会报异常。所以,要保证p2.next不为nullif (p1 == p2) {return true;}}return false;}
判断有环单链表中,环的长度
思想:可以利用hasCycle的思路,如果找到相遇的结点了。那么p2从相遇的结点开始,再次走到此结点时走的距离,就是环的长度了。
从交点和相遇点推导步长规律
思想:对于有环链表,我们假设用环的起始结点startNode和相遇结点kNode(用hasCycle方法模拟走一下就知道了),将链表分为3段。分别是k1,k2,k3.具体见下图:
假设判断其相遇时(hasCycle方法),一共走了k步。那么
- p1走的步数:
k1 + k2 = k
; - p2走的步数:
k1 + 2K2 + k3 = 2k
;
这样,两个公式联立就得k1 = k3
.
换句话说,当两个结点相遇的时候,将p1再重新指向head,p1和p2一起往后移动,当再次相遇的时候,该结点就是环的起点。123456789101112131415161718192021222324252627282930313233/*** 获取单链表中,环的起始结点* test:(null), (偶数个元素、偶数环元素的链表), (偶数个元素且无环的链表),(奇数个元素、奇数环的链表),* (奇数个元素且无环的链表),(1个元素的无环链表),(1个元素有环链表),(只有2个元素且有环的链表),(圈链表)** @param head 待处理的链表* @return Node 环的起始结点。 若没有环,则返回null*/public static Node getCycleStartNode2(Node head) {if (head == null || head.next == null) {return null;}Node p1 = head;Node p2 = head;//找到相遇的结点。while (p2 != null && p2.next != null) {p1 = p1.next;p2 = p2.next.next;if (p1 == p2) {break;}}if (p2 == null || p2.next == null) return null; //没有环//将p1重新指向head,p1和p2一起走,直到两个指针再次相遇p1 = head;while (p1 != p2) {p1 = p1.next;p2 = p2.next;}return p2;}
配合HashSet一次遍历
思想:可以在遍历的时候,将链表结点放入HashSet中,当遍历时发现有此元素,则该结点就是环的起始结点。这种方法很简洁,但是当结点比较多的时候,空间开销太大。
删除单链表中指定结点
这个问题有个特殊要求,就是要求时间复杂度O(1)
常规的思路:对于这样一段链表:n0 -> n1 -> n2 -> n3 -> n4
如果要删除结点n2
,可以让n1
指向n3
。然后释放n2
。这样的话需要找到结点n1
,时间复杂度为O(n)
思想:对于单链表,可以将n3
复制到n2
,然后删除n3
即可。这样的方法在java中,如果是删除链表最后一个元素,仍然需要找前面的元素。
好了,先总结到这里吧。
参考资料:
http://blog.csdn.net/fightforyourdream/article/details/16353519
http://www.jb51.net/article/71885.htm