数据结构——链表
Posted ITRenj
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构——链表相关的知识,希望对你有一定的参考价值。
在《数据结构——链表(一)》一文中,我们介绍了链表的基本操作与实现,这篇文章我们来进一步学习链表的一些操作
链表进阶
判断单向链表是否有环
快慢指针法: 定义两个指针,通过两个指针的移动速度不同来实现功能,常用来寻找中间节点(快指针移动速度是慢指针的两倍,快指针移动到结尾时,慢指针刚好在中间)、判断单链表是否有环等操作。
利用快慢指针法判断单链表是否有环的代码实现如下:
public boolean hasCircle()
if (header == null) return false;
if (tail == null) return false;
Node<T> fast = header;
Node<T> slow = header;
do
if (fast == null) return false;
fast = fast.next;
if (fast == null) return false;
fast = fast.next;
slow = slow.next;
while (fast != slow);
return true;
单链表反转
单链表反转的实现有多种方法,这里主要介绍四种:迭代反转法、递归反转法、头插入反转法以及就地逆置反转法。
迭代反转链表
该方法就是借助三个节点(beg、mid、end),让mid节点从当前链表的头节点开始,一直遍历至链表的最后一个节点,这期间会逐个改变所遍历到的节点的指针域,令其指向前一个节点,直至 mid 指向链表中最后一个节点。
- 初始定义三个节点(beg、mid、end)并初始化
在上图的基础上,遍历链表的过程就等价为:3 个指针每次各向后移动一个节点,直至 mid 指向链表中最后一个节点(此时 end 为 NULL )。需要注意的是,这 3 个指针每移动之前,都需要做一步操作,即改变 mid 所指节点的指针域,令其指向 beg。
- 改变 mid 所指节点的指针域
- 将3 个指针各向后移动一个节点
- 直至 mid 指向链表中最后一个节点,此时end指向NULL
- 最后只需改变 head 头指针的指向,另其和 mid 相同,就实现了链表的反转。
Java代码实现如下:
public void reverseLinkedListIteration()
if (header == null) return;
if (header.next == null) return;
if (tail == null) return;
final Node<T> saveHeader = header;
Node<T> beg = null;
Node<T> mid = header;
Node<T> end = header.next;
while (true)
mid.next = beg; // 修改mid节点指向
// end == null 表示非循环链表结束 end == saveHeader 表示循环链表结束
if (end == null || end == saveHeader) break;
// 整体移动
beg = mid;
mid = end;
end = end.next;
header = mid;
tail = saveHeader;
// tail.next = null; // 不是循环链表
tail.next = header; // 是循环链表
递归反转链表
和迭代反转法的思想恰好相反,递归反转法的实现思想是从链表的尾节点开始,依次向前遍历,遍历过程依次改变各节点的指向,即另其指向前一个节点。使用了递归,先看代码实现在对照图片可能更容易理解。
Java代码实现如下:
// 递归反转法
public void reverseLinkedListRecursive()
final Node<T> saveHeader = header;
header = reverseNode(saveHeader, header, true);
tail = saveHeader;
// tail.next = null; // 不是循环链表
tail.next = header; // 是循环链表
// 递归节点
private Node<T> reverseNode(Node<T> oldHeader, Node<T> header, boolean firstCall)
// 链表递归出口
if (header == null || header.next == null) return header;
// header.next == oldHeader 表示循环链表递归出口 增加 firstCall 是防止第一次执行就直接返回了
if (!firstCall && header.next == oldHeader)
return header;
// 一直递归,找到链表中最后一个节点
Node<T> newHeader = reverseNode(oldHeader, header.next, false);
// 每退出一层,都需要改变 head->next 节点指针域的指向,同时令 head 所指节点的指针域为 NULL。
header.next.next = header;
header.next = null;
// 每一层递归结束,都要将新的头指针返回给上一层。由此,即可保证整个递归过程中,能够一直找得到新链表的表头。
return newHeader;
- 初始状态链表如下
- 到达递归出口时状态
- 递归往上进行指针域第一次修改完成时状态,上述代码22-23行
- 递归完成时,链表反转完成
- 修改header和tail的值,上述代码5-7行
头插入法反转链表
所谓头插法,是指在原有链表的基础上,依次将位于链表头部的节点摘下,然后采用从头部插入的方式生成一个新链表,则此链表即为原链表的反转版。
- 创建一个新的空链表
- 从原链表中摘除头部节点 1,然后将原来的header向后移动一位;最后新表的头用摘出的节点替换
- 从原链表中摘除头部节点 2,然后将原来的header向后移动一位;将节点2的指针域指向节点1,并且用节点2作为新的头
- 最终状态,原来的header节点指向NULL
- 修改header和tail的值
Java代码实现如下:
public void reverseLinkedListHeaderInsert()
if (header == null || header.next == null) return;
final Node<T> saveHeader = header;
Node<T> newHeader = null;
Node<T> temp = null;
while (true)
temp = header;
header = header.next;
temp.next = newHeader;
newHeader = temp;
// 到达结尾 header == saveHeader 表示循环链表达到结尾
if (header == null || header == saveHeader) break;
header = newHeader;
tail = saveHeader;
// tail.next = null; // 不是循环链表
tail.next = header; // 是循环链表
就地逆置反转法
就地逆置法和头插法的实现思想类似,唯一的区别在于,头插法是通过建立一个新链表实现的,而就地逆置法则是直接对原链表做修改,从而实现将原链表反转。需要借助两个指针(定义为beg、end)
- 初始状态下,令 beg 指向第一个节点,end 指向 beg->next
- 将 end 所指节点 2 从链表上摘除,然后再添加至当前链表的头部
- 修改header和end的指向,用end=end.next 来移动指针
- 不断重复第一步(第2点)和第二步(第3点),直到end指向NULL,最终实现反转
- 修改header和tail的值
Java代码实现如下:
public void reverseLinkedListLocalReverse()
if (header == null || header.next == null) return;
tail.next = null; // 先断开循环
Node<T> beg = header;
Node<T> end = header.next;
while (true)
// 移动节点
beg.next = end.next;
end.next = header;
// 修改指针
header = end;
end = beg.next;
if (end == null) break;
// 组成循环
tail = beg;
tail.next = header;
用到的算法和思路
二分法思路
二分法查找是一种非常高效的搜索方法,主要原理是每次搜索可以抛弃一半的值来缩小范围。其时间复杂度是O(log2n)。
二分法的适用情况一般满足以下几点:
- 该数组数据量巨大,需要对处理的时间复杂度进行优化;
- 该数组已经排序;
- 一般要求找到的是某一个值或一个位置。
快慢指针法
快慢指针是定义两个指针,通过两个指针的移动速度不同来实现功能,常用来寻找中间节点(快指针移动速度是慢指针的两倍,快指针移动到结尾时,慢指针刚好在中间)、判断单链表是否有环等操作。
迭代
重复反馈过程的活动,每一次迭代的结果会作为下一次迭代的初始值。不断用变量的旧值递推新值的过程。
**迭代程序必须考虑的问题:**不能让迭代过程无休止地重复执行下去。
迭代过程的控制通常可分为两种情况:
- 所需的迭代次数是个可以计算出来的确定值,可以构建一个固定次数的循环来实现迭代
- 所需的迭代次数无法确定,对于后一种情况,根据情况分析结束迭代过程的条件
举例:
阶乘:n! = n * (n-1) * (n-2) * …* 1(n>0)
public int iterationFactorial(int factorialValue)
int result = factorialValue;
for (int i = factorialValue; i > 1; i--)
result = result * (i - 1);
return result;
递归
在数学和计算机科学中,指的是在函数调用自身。把问题转化为规模缩小了的同类问题的子问题。递归:有递(去)有归(回)
构成递归需具备的条件:
- 子问题须与原始问题为同样的事,且规模更小
- 不能无限制地调用本身,须有个出口
举例:
-
阶乘:n! = n * (n-1) * (n-2) * …* 1(n>0)
public int recursiveFactorial(int factorialValue) if (factorialValue <= 1) return factorialValue; return factorialValue * recursiveFactorial(factorialValue - 1);
-
斐波纳契数列:又称黄金分割数列,指的是这样一个数列:1、1、2、3、5、8、13、21、……(这个数列从第三项开始,每一项都等于前两项之和)
public int recursiveFibonacci(int number) if (number <= 0) return 0; if ((number == 1) || (number == 2)) return 1; return recursiveFibonacci(number - 2) + recursiveFibonacci(number - 1); // 打印 10 个斐波纳契数据 for (int counter = 1; counter <= 10; counter++) System.out.printf("Fibonacci of %d is: %d\\n", counter, recursiveFibonacci(counter));
递归与迭代的关系:
- 递归中一定有迭代,但是迭代中不一定有递归,换句话说,就是所有的迭代可以转换为递归,但递归不一定可以转换成迭代。
- 能用迭代就不用递归,迭代时间只因循环次数增加而增加,而且没有额外的空间开销;递归调用浪费了空间,而且递归太深容易造成堆栈的溢出
完整代码实现及测试(java)
单向链表实现:SingleLinkedList.java
双向链表实现:DoubleLinkedList.java
测试代码:LikedListTest.java
以上是关于数据结构——链表的主要内容,如果未能解决你的问题,请参考以下文章