数据结构——链表

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 指向链表中最后一个节点。

  1. 初始定义三个节点(beg、mid、end)并初始化

在上图的基础上,遍历链表的过程就等价为:3 个指针每次各向后移动一个节点,直至 mid 指向链表中最后一个节点(此时 end 为 NULL )。需要注意的是,这 3 个指针每移动之前,都需要做一步操作,即改变 mid 所指节点的指针域,令其指向 beg。

  1. 改变 mid 所指节点的指针域

  1. 将3 个指针各向后移动一个节点

  1. 直至 mid 指向链表中最后一个节点,此时end指向NULL

  1. 最后只需改变 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;

  1. 初始状态链表如下

  1. 到达递归出口时状态

  1. 递归往上进行指针域第一次修改完成时状态,上述代码22-23行

  1. 递归完成时,链表反转完成

  1. 修改header和tail的值,上述代码5-7行

头插入法反转链表

所谓头插法,是指在原有链表的基础上,依次将位于链表头部的节点摘下,然后采用从头部插入的方式生成一个新链表,则此链表即为原链表的反转版。

  1. 创建一个新的空链表

  1. 从原链表中摘除头部节点 1,然后将原来的header向后移动一位;最后新表的头用摘出的节点替换

  1. 从原链表中摘除头部节点 2,然后将原来的header向后移动一位;将节点2的指针域指向节点1,并且用节点2作为新的头

  1. 最终状态,原来的header节点指向NULL

  1. 修改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)

  1. 初始状态下,令 beg 指向第一个节点,end 指向 beg->next

  1. 将 end 所指节点 2 从链表上摘除,然后再添加至当前链表的头部

  1. 修改header和end的指向,用end=end.next 来移动指针

  1. 不断重复第一步(第2点)和第二步(第3点),直到end指向NULL,最终实现反转

  1. 修改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)。

二分法的适用情况一般满足以下几点:

  1. 该数组数据量巨大,需要对处理的时间复杂度进行优化;
  2. 该数组已经排序;
  3. 一般要求找到的是某一个值或一个位置。

快慢指针法

快慢指针是定义两个指针,通过两个指针的移动速度不同来实现功能,常用来寻找中间节点(快指针移动速度是慢指针的两倍,快指针移动到结尾时,慢指针刚好在中间)、判断单链表是否有环等操作。

迭代

重复反馈过程的活动,每一次迭代的结果会作为下一次迭代的初始值。不断用变量的旧值递推新值的过程。

**迭代程序必须考虑的问题:**不能让迭代过程无休止地重复执行下去。

迭代过程的控制通常可分为两种情况:

  1. 所需的迭代次数是个可以计算出来的确定值,可以构建一个固定次数的循环来实现迭代
  2. 所需的迭代次数无法确定,对于后一种情况,根据情况分析结束迭代过程的条件

举例:

阶乘: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;

递归

在数学和计算机科学中,指的是在函数调用自身。把问题转化为规模缩小了的同类问题的子问题。递归:有递(去)有归(回)

构成递归需具备的条件:

  1. 子问题须与原始问题为同样的事,且规模更小
  2. 不能无限制地调用本身,须有个出口

举例:

  1. 阶乘:n! = n * (n-1) * (n-2) * …* 1(n>0)

     public int recursiveFactorial(int factorialValue) 
         if (factorialValue <= 1) return factorialValue;
         return factorialValue * recursiveFactorial(factorialValue - 1);
     
    
  2. 斐波纳契数列:又称黄金分割数列,指的是这样一个数列: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));
     
    

递归与迭代的关系:

  1. 递归中一定有迭代,但是迭代中不一定有递归,换句话说,就是所有的迭代可以转换为递归,但递归不一定可以转换成迭代。
  2. 能用迭代就不用递归,迭代时间只因循环次数增加而增加,而且没有额外的空间开销;递归调用浪费了空间,而且递归太深容易造成堆栈的溢出

完整代码实现及测试(java)

单向链表实现:SingleLinkedList.java

双向链表实现:DoubleLinkedList.java

测试代码:LikedListTest.java

以上是关于数据结构——链表的主要内容,如果未能解决你的问题,请参考以下文章

邻接表的简介

数据结构问题 在邻接表中啥是表节点?啥是表头节点?啥是头节点?

0508数据结构

0508数据结构

linux strncpy()和strcat使用总结

数据结构&算法07-链表技巧&参考源码