【leetcode】(四)排序算法汇总和链表

(一)排序算法的稳定性及其汇总

1.稳定性

同样值的个体之间,如果不因为排序而改变相对次序,就是这个排序是有稳定性的;否则就没有。例如:

原数组2,1,2,1,3,2,3,2,排完序后变成新数组1,1,2,2,2,2,3,3,要保持稳定性,是指原数组中的第一个"1"和第二个"1"在排完序后是否能够对应新数组中的第一个"1"和第二个"1",同样的,2和3以此类推。

对于基础数据类型来说,稳定性没有用处。但对于非基础数据类型来说,稳定性有作用。

例如:一些班级里有一批学生,每个学生有class和age属性,第一次排序的时候,按照age从小到大排序;第二次排序的时候,按照class排序。如果两轮排序之后,同一个班级内部的学生是按照age从小到大排序的,那么这个排序称为"稳定的排序"。

2.不具备稳定性的排序

选择排序、快速排序、堆排序

(1)选择排序

举例:3,3,3,3,3,1,3,3,3,3,3,对应序列编号{a1,a2,a3,a3,a4,a5,a6,a7,a8,a9,a10,a11},在选择排序后,变成1,3,3,3,3,3,3,3,3,3,3,对应序列编号{a6,a2,a3,a3,a4,a5,a1,a7,a8,a9,a10,a11},原来的序列被打断,没有做到有序,故不稳定。

(2)快速排序

举例:6{a},7,6{b},6{c},3,5,以5为划分值。

划分过程中,6{a}>5,会和后面的3发生远距离交换,

数组变为3,7,6{b},6{c},6{a},5

原来三个6的顺序为:

6{a}→6{b}→6{c}

交换后变为:

6{b}→6{c}→6{a}

相同元素的相对顺序被破坏,故快速排序不稳定。

(3)堆排序

举例:5,4{a},4{b},6

建立大根堆时,当6插入到4{a}下面:

5

/ \

4{a} 4{b}

/

6

因为6>4{a},交换:

5

/ \

6 4{b}

/

4{a}

继续6>5,再交换:

6

/ \

5 4{b}

/

4{a}

原来的4顺序为:

4{a} → 4{b}

堆调整过程中变为:

4{b} → 4{a}

继续完成堆排序后可得到:

4{b},4{a},5,6

相同的4相对次序发生改变,故堆排序不稳定。

3.具备稳定性的排序

冒泡排序、插入排序、归并排序、一切桶排序思想下的排序

(1)冒泡排序

举例:6,5,4,5,3,4,6,对应序列{0,1,2,3,4,5,6},一轮排序后变成5,4,5,3,4,6,6,原序列的6{0}和6{6}的序列不变。

(2)插入排序

举例:3,2,2对应{0,1,2},一轮排序后变成2,2,3,对应序列{1,2,0},2的序列不变。

(3)归并排序

举例:3,2,4,2,对应序列{0,1,2,3}。

拆分并分别排序后得到:

2{1},32{3},4

merge时,两个2相等,因为相等时优先拷贝左侧元素,

所以最终得到:

2{1},2{3},3,4

对应序列{1,3,0,2}。

两个2原来的相对次序没有改变,故归并排序稳定。

4.排序的时空复杂度和稳定性总结

|--------|------------|---------|-----|
| | 时间复杂度 | 空间复杂度 | 稳定性 |
| 选择 | O(N^2) | O(1) | × |
| 冒泡 | O(N^2) | O(1) | √ |
| 插入 | O(N^2) | O(1) | √ |
| 归并 | O(N*logN) | O(N) | √ |
| 快排(随机) | O(N*logN) | O(logN) | × |
| 堆 | O(N*logN) | O(1) | × |

目前没有找到时间复杂度O(N*logN),额外空间复杂度O(1),又稳定的排序。

常见的坑

(1),归并排序的额外空间复杂度可以变成O(1),但是非常难且变完之后不再稳定,不需要掌握,有兴趣可以搜"归并排序 内部缓存法"

(2),"原地归并排序"的帖子都是垃圾,会让归并排序的时间复杂度变成O(N^2)

(3),快速排序可以做到稳定性问题,但是非常难但是空间复杂度会变成O(N),不需要掌握,可以搜"01 stable sort"

(4),所有的改进都不重要,因为目前没有找到时间复杂度O(N*logN),额外空间复杂度O(1),又稳定的排序。

(5),有一道题目,是否奇数放在数组左边,偶数放在数组右边,还要求原始的相对次序不变,碰到这个问题,可以怼面试官。这道题本质上是"01 stable sort"问题: 奇数映射成0,偶数映射成1,对0和1做稳定排序。 若允许O(N)额外空间,可以O(N)时间轻松实现; 若进一步要求O(1)额外空间并保持稳定,则实现非常困难, 属于01 stable sort / stable partition问题。

工程上对排序的改进

(1)充分利用O(N*logN)和O(^2)排序各自的优势(综合排序)

|-----|----|------------|
| 样本量 | 调度 | 复杂度 |
| 大样本 | 快排 | O(N*logN) |
| 小样本 | 插入 | O(N^2) |

示例代码

java 复制代码
package class004;

import java.util.Arrays;

public class Test {

    // 对整个数组排序
    public static void quickSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        quickSort(arr, 0, arr.length - 1);
    }

    public static void quickSort(int[] arr, int l, int r) {
        // 空区间或只有一个数
        if (l >= r) {
            return;
        }

        // 小样本使用插入排序
        // O(N^2)虽然复杂度高,但N很小时常数项小,实际跑得快
        if (r - l + 1 <= 60) {
            insertionSort(arr, l, r);
            return;
        }

        // 随机选择一个数,与最后位置交换
        swap(arr,
                l + (int) (Math.random() * (r - l + 1)),
                r);

        // 荷兰国旗划分
        int[] p = partition(arr, l, r);

        // <区继续排序
        quickSort(arr, l, p[0] - 1);

        // >区继续排序
        quickSort(arr, p[1] + 1, r);
    }

    // 对arr[L...R]进行插入排序
    public static void insertionSort(int[] arr, int L, int R) {
        for (int i = L + 1; i <= R; i++) {
            for (int j = i - 1;
                 j >= L && arr[j] > arr[j + 1];
                 j--) {
                swap(arr, j, j + 1);
            }
        }
    }

    /*
     * 处理arr[L...R]
     * 默认以arr[R]作为划分值p
     *
     * 最终:
     *
     * < p | == p | > p
     *
     * 返回等于区域:
     * [左边界, 右边界]
     */
    public static int[] partition(int[] arr, int L, int R) {

        int less = L - 1; // <区右边界
        int more = R;     // >区左边界

        // L同时作为当前遍历位置
        while (L < more) {

            // 当前数 < 划分值
            if (arr[L] < arr[R]) {
                swap(arr, ++less, L++);

                // 当前数 > 划分值
            } else if (arr[L] > arr[R]) {
                swap(arr, --more, L);

                // 当前数 == 划分值
            } else {
                L++;
            }
        }

        // 把最后位置的划分值放入等于区
        swap(arr, more, R);

        // 等于区:[less+1, more]
        return new int[]{less + 1, more};
    }

    public static void swap(int[] arr, int i, int j) {
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }

    public static void main(String[] args) {

        // 测试1:普通数组
        int[] arr1 = {
                11, 43, 32, 12, 24,
                6, 3, 8, 1, 7
        };

        System.out.println("排序前:" + Arrays.toString(arr1));
        quickSort(arr1);
        System.out.println("排序后:" + Arrays.toString(arr1));

        System.out.println("--------------------");

        // 测试2:有重复值
        int[] arr2 = {
                5, 3, 7, 3, 9, 5, 2, 5, 1, 3
        };

        System.out.println("排序前:" + Arrays.toString(arr2));
        quickSort(arr2);
        System.out.println("排序后:" + Arrays.toString(arr2));

        System.out.println("--------------------");

        // 测试3:倒序数组
        int[] arr3 = {
                10, 9, 8, 7, 6, 5, 4, 3, 2, 1
        };

        System.out.println("排序前:" + Arrays.toString(arr3));
        quickSort(arr3);
        System.out.println("排序后:" + Arrays.toString(arr3));

        System.out.println("--------------------");

        // 测试4:超过60个元素,真正测试"快排+插入排序"
        int[] arr4 = new int[100];

        for (int i = 0; i < arr4.length; i++) {
            arr4[i] = (int) (Math.random() * 100);
        }

        System.out.println("排序前:" + Arrays.toString(arr4));
        quickSort(arr4);
        System.out.println("排序后:" + Arrays.toString(arr4));
    }
}

运行结果:

java 复制代码
排序前:[11, 43, 32, 12, 24, 6, 3, 8, 1, 7]
排序后:[1, 3, 6, 7, 8, 11, 12, 24, 32, 43]
--------------------
排序前:[5, 3, 7, 3, 9, 5, 2, 5, 1, 3]
排序后:[1, 2, 3, 3, 3, 5, 5, 5, 7, 9]
--------------------
排序前:[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
排序后:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
--------------------
排序前:[61, 43, 77, 33, 60, 67, 77, 48, 99, 50, 8, 18, 6, 95, 86, 3, 77, 61, 17, 98, 87, 71, 78, 0, 16, 66, 53, 48, 32, 47, 57, 83, 87, 35, 89, 27, 58, 66, 33, 42, 18, 37, 91, 7, 84, 17, 77, 58, 77, 25, 93, 74, 27, 15, 51, 99, 57, 63, 16, 96, 86, 17, 60, 84, 47, 38, 0, 9, 34, 11, 14, 48, 27, 0, 42, 65, 69, 55, 89, 91, 59, 11, 50, 23, 79, 32, 39, 94, 44, 94, 59, 98, 52, 49, 81, 28, 70, 13, 74, 93]
排序后:[0, 0, 0, 3, 6, 7, 8, 9, 11, 11, 13, 14, 15, 16, 16, 17, 17, 17, 18, 18, 23, 25, 27, 27, 27, 28, 32, 32, 33, 33, 34, 35, 37, 38, 39, 42, 42, 43, 44, 47, 47, 48, 48, 48, 49, 50, 50, 51, 52, 53, 55, 57, 57, 58, 58, 59, 59, 60, 60, 61, 61, 63, 65, 66, 66, 67, 69, 70, 71, 74, 74, 77, 77, 77, 77, 77, 78, 79, 81, 83, 84, 84, 86, 86, 87, 87, 89, 89, 91, 91, 93, 93, 94, 94, 95, 96, 98, 98, 99, 99]

实测时间消耗(以60为划分):

java 复制代码
package class004;

import java.util.Arrays;

public class Test01 {

    // =========================
    // 调整前:纯快速排序
    // =========================
    public static void quickSortBefore(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        quickSortBefore(arr, 0, arr.length - 1);
    }

    public static void quickSortBefore(int[] arr, int l, int r) {
        if (l >= r) {
            return;
        }

        // 不做小样本优化
        // 所有区间一直使用快速排序
        swap(arr,
                l + (int) (Math.random() * (r - l + 1)),
                r);

        int[] p = partition(arr, l, r);

        quickSortBefore(arr, l, p[0] - 1);
        quickSortBefore(arr, p[1] + 1, r);
    }


    // =========================
    // 调整后:快排 + 插入排序
    // =========================
    public static void quickSortAfter(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        quickSortAfter(arr, 0, arr.length - 1);
    }

    public static void quickSortAfter(int[] arr, int l, int r) {
        if (l >= r) {
            return;
        }

        // 小样本直接使用插入排序
        if (r - l + 1 <= 60) {
            insertionSort(arr, l, r);
            return;
        }

        swap(arr,
                l + (int) (Math.random() * (r - l + 1)),
                r);

        int[] p = partition(arr, l, r);

        quickSortAfter(arr, l, p[0] - 1);
        quickSortAfter(arr, p[1] + 1, r);
    }


    // 对arr[L...R]进行插入排序
    public static void insertionSort(int[] arr, int L, int R) {
        for (int i = L + 1; i <= R; i++) {
            for (int j = i - 1;
                 j >= L && arr[j] > arr[j + 1];
                 j--) {
                swap(arr, j, j + 1);
            }
        }
    }


    // 荷兰国旗划分
    public static int[] partition(int[] arr, int L, int R) {
        int less = L - 1;
        int more = R;

        while (L < more) {
            if (arr[L] < arr[R]) {
                swap(arr, ++less, L++);
            } else if (arr[L] > arr[R]) {
                swap(arr, --more, L);
            } else {
                L++;
            }
        }

        swap(arr, more, R);

        return new int[]{less + 1, more};
    }


    public static void swap(int[] arr, int i, int j) {
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }


    public static void main(String[] args) {
        // =========================
        // 测试4:调整前 VS 调整后
        // =========================

        int size = 100000;

        // 生成原始随机数组
        int[] arr4 = new int[size];

        for (int i = 0; i < arr4.length; i++) {
            arr4[i] = (int) (Math.random() * 100000);
        }

        // 必须复制两份完全相同的数据
        // 保证两种算法处理的是同一个数组
        int[] arr4Before = Arrays.copyOf(arr4, arr4.length);
        int[] arr4After = Arrays.copyOf(arr4, arr4.length);


        // =========================
        // 调整前:纯快速排序
        // =========================

        long start1 = System.nanoTime();

        quickSortBefore(arr4Before);

        long end1 = System.nanoTime();

        long timeBefore = end1 - start1;


        // =========================
        // 调整后:快排 + 插入排序
        // =========================

        long start2 = System.nanoTime();

        quickSortAfter(arr4After);

        long end2 = System.nanoTime();

        long timeAfter = end2 - start2;


        // =========================
        // 打印结果
        // =========================

        System.out.println("测试数据量:" + size);

        System.out.println(
                "调整前(纯快速排序)耗时:" +
                        timeBefore / 1_000_000.0 +
                        " ms"
        );

        System.out.println(
                "调整后(快排+插入排序)耗时:" +
                        timeAfter / 1_000_000.0 +
                        " ms"
        );

        System.out.println(
                "两种排序结果是否一致:" +
                        Arrays.equals(arr4Before, arr4After)
        );
    }
}

时间消耗(以60为划分,测试数据量达到100000):

java 复制代码
第一次:

测试数据量:100000
调整前(纯快速排序)耗时:9.4522 ms
调整后(快排+插入排序)耗时:7.222 ms
两种排序结果是否一致:true

第二次:
测试数据量:100000
调整前(纯快速排序)耗时:8.8005 ms
调整后(快排+插入排序)耗时:6.7178 ms
两种排序结果是否一致:true

时间消耗(以20为划分,测试数据量达到1000000):

java 复制代码
第一次:

测试数据量:1000000
调整前(纯快速排序)耗时:66.0448 ms
调整后(快排+插入排序)耗时:63.305 ms
两种排序结果是否一致:true

第二次:

测试数据量:1000000
调整前(纯快速排序)耗时:60.3482 ms
调整后(快排+插入排序)耗时:57.6581 ms
两种排序结果是否一致:true

时间消耗(以60为划分,测试数据量达到10000000):

java 复制代码
第一次

测试数据量:10000000
调整前(纯快速排序)耗时:553.9916 ms
调整后(快排+插入排序)耗时:539.2518 ms
两种排序结果是否一致:true

第二次

测试数据量:10000000
调整前(纯快速排序)耗时:626.0049 ms
调整后(快排+插入排序)耗时:548.3475 ms
两种排序结果是否一致:true

时间消耗(以20为划分):

通过以上测试我们知道,以60为划分,在测试数据量达到100000时,调整后平均能够节约2ms;以20为划分,在测试数据量达到1000000时,调整后平均能够节约3ms;以30为划分,在测试数据量达到10000000时,调整后平均能够节约10ms以上。

(2)稳定性的考虑

面试问题:为什么在对基础类型进行排序时,采用快速排序;在面对非基础类型的数据时,采用归并排序?原因:稳定性的需要。

Java 底层排序同样采用综合排序思想。以 Arrays.sort(int[]) 为例,主要采用 Dual-Pivot Quicksort,但小规模区间会切换到插入排序等方法;当快速排序出现恶化风险时还可以使用堆排序兜底。因此工程中的排序通常不是单一算法,而是根据样本规模和数据特征动态选择算法。

(二)链表

1.哈希表的简单介绍

1)哈希表在使用层面上可以理解为一种集合结构

2)如果只有key,没有伴随数据value,可以使用HashSet结构(C++中叫UnOrderedSet)

3)如果既有key,又有伴随数据value,可以使用HashMap结构(C++中叫UnOrderedMap)

4)有无伴随数据,是HashMap和HashSet唯一的区别,底层的实际结构是一回事

5)使用哈希表增(put)、删(remove)、改(put)和查(get)的操作,可以认为时间复杂度为O(1),但是常数时间比较大

6)放入哈希表的东西,如果是基础类型,内部按值传递,内存占用就是这个东西的大小

7)放入哈希表的东西,如果不是基础类型,内部按引用传递,内存占用是这个东西内存地址的大小(一律只占八字节,和内存占用大小没有关系)

有关哈希表的原理,将在提升班"与哈希函数有关的数据结构"一章中讲叙原理。

2.有序表的简单介绍

1)有序表在使用层面上可以理解为一种集合结构

2)如果只有key,没有伴随数据value,可以使用TreeSet结构(C++中叫OrderedSet)

3)如果既有key,又有伴随数据value,可以使用TreeMap结构(C++中叫OrderedMap)

4)有无伴随数据,是TreeSet和TreeMap唯一的区别,底层的实际结构是一回事

5)有序表和哈希表的区别是,有序表把key按照顺序组织起来,而哈希表完全不组织

6)红黑树、AVL树、size-balance-tree和跳表等都属于有序表结构,只是底层具体实现不同

7)放入哈希表的东西,如果是基础类型,内部按值传递,内存占用就是这个东西的大小

8)放入哈希表的东西,如果不是基础类型,必须提供比较器,内部按引用传递,内存占用是这个东西内存地址的大小

9)不管是什么底层具体实现,只要是有序表,都有以下固定的基本功能和固定的时间复杂度

3.有序表的固定操作

1)void put(K key, V value):将一个(key,value)记录加入到表中,或者将key的记录更新成value。

2)V get(K key):根据给定的key,查询value并返回。

3)void remove(K key):移除key的记录。

4)boolean containsKey(K key):询问是否有关于key的记录。

5)K firstKey():返回所有键值的排序结果中,最左(最小)的那个。

6)K lastKey():返回所有键值的排序结果中,最右(最大)的那个。

7)K floorKey(K key):如果表中存入过key,返回key;否则返回所有键值的排序结果中,key的前一个。

8)K ceilingKey(K key):如果表中存入过key,返回key;否则返回所有键值的排序结果中,key的后一个。

以上所有操作时间复杂度都是O(logN),N为有序表含有的记录数

有关有序表的原理,将在提升班"有序表详解"一章中讲叙原理。

哈希表,有序表相关代码:

java 复制代码
package class004;

import java.awt.*;
import java.util.*;

public class Code_HashAndTree {

    public static class Node{
        public int value;
        public Node next;

        public Node(int val){
            value=val;
        }
    }
    public static class NodeComparator implements Comparator<Node>{
        @Override
        public int compare(Node o1,Node o2){
            return o1.value-o2.value;
        }
    }
    public static void main(String[] args){
        Node nodeA=null;
        Node nodeB=null;
        Node nodeC=null;

        //UnOrderedMap,UnSortedMap,UnOrderedSet,UnSortedSet ->C++
        //hashSet1的key是基础类型-》int类型
        HashSet<Integer>hashSet1=new HashSet<>();
        hashSet1.add(3);
        System.out.println(hashSet1.contains(3));
        hashSet1.remove(3);
        System.out.println(hashSet1.contains(3));

        HashMap<Integer,String>mapTest=new HashMap<>();

        mapTest.put(1,"zuo");
        mapTest.put(1,"cheng");
        mapTest.put(2,"2");

        System.out.println(mapTest.containsKey(1));
        System.out.println(mapTest.get(1));
        System.out.println(mapTest.get(4));

        mapTest.remove(2);
        System.out.println(mapTest.get(2));

        System.out.println("===========1==========");

        //hashSet2的key是非基础类型->Node类型
        nodeA=new Node(1);
        nodeB=new Node(1);
        HashSet<Node>hashSet2=new HashSet<>();
        hashSet2.add(nodeA);
        System.out.println(hashSet2.contains(nodeA));
        System.out.println(hashSet2.contains(nodeB));
        hashSet2.remove(nodeA);
        System.out.println(hashSet2.contains(nodeA));
        System.out.println("===========2==========");

        //hashMap1的key是基础类型-》String类型
        HashMap<String,Integer>hashMap1=new HashMap<>();
        String str1="key";
        String str2="key";
        hashMap1.put(str1,1);
        System.out.println(hashMap1.containsKey(str1));
        System.out.println(hashMap1.containsKey(str2));
        System.out.println(hashMap1.get(str1));
        System.out.println(hashMap1.get(str2));

        hashMap1.put(str2,2);
        System.out.println(hashMap1.containsKey(str1));
        System.out.println(hashMap1.containsKey(str2));
        System.out.println(hashMap1.get(str1));
        System.out.println(hashMap1.get(str2));

        hashMap1.remove(str1);
        System.out.println(hashMap1.containsKey(str1));
        System.out.println(hashMap1.containsKey(str2));
        System.out.println("===========3==========");

        //hashMap2的key是非基础类型-》Node类型
        nodeA=new Node(1);
        nodeB=new Node(1);
        HashMap<Node,String>hashMap2=new HashMap<>();
        hashMap2.put(nodeA,"A节点");
        System.out.println(hashMap2.containsKey(nodeA));
        System.out.println(hashMap2.containsKey(nodeB));
        System.out.println(hashMap2.get(nodeA));
        System.out.println(hashMap2.get(nodeB));
        hashMap2.put(nodeB,"B节点");
        System.out.println(hashMap2.containsKey(nodeA));
        System.out.println(hashMap2.containsKey(nodeB));
        System.out.println(hashMap2.get(nodeA));
        System.out.println(hashMap2.get(nodeB));
        System.out.println("===========4==========");

        //treeSet的key是非基础类型-》Node类型
        nodeA=new Node(5);
        nodeB=new Node(3);
        nodeC=new Node(7);

        TreeSet<Node>treeSet=new TreeSet<>();//红黑树
        //以下代码会报错,因为没有提供Node类型的比较器
        try{
            treeSet.add(nodeA);
            treeSet.add(nodeB);
            treeSet.add(nodeC);
        }catch (Exception e){
            System.out.println("错误信息:"+e.getMessage());
        }
        treeSet=new TreeSet<>(new NodeComparator());
        //以下代码没问题,因为提供了Node类型的比较器
        try{
            treeSet.add(nodeA);
            treeSet.add(nodeB);
            treeSet.add(nodeC);
            System.out.println("这次节点都加入了");
        }catch (Exception e){
            System.out.println("错误信息:"+e.getMessage());
        }
        System.out.println("===========5==========");

        //展示有序表常用操作,性能O(logN)级别
        TreeMap<Integer,String>treeMap1=new TreeMap<>();
        treeMap1.put(7,"我是7");
        treeMap1.put(5,"我是5");
        treeMap1.put(4,"我是4");
        treeMap1.put(3,"我是3");
        treeMap1.put(9,"我是9");
        treeMap1.put(2,"我是2");
        System.out.println(treeMap1.containsKey(5));
        System.out.println(treeMap1.get(5));
        System.out.println(treeMap1.firstKey()+",我最小");
        System.out.println(treeMap1.lastKey()+",我最大");
        System.out.println(treeMap1.floorKey(8)+",在表中所有<=8的数中,我离8最近");
        System.out.println(treeMap1.ceilingKey(8)+",在表中所有>=8的数中,我离8最近");
        System.out.println(treeMap1.floorKey(7)+",在表中所有<=7的数中,我离7最近");
        System.out.println(treeMap1.ceilingKey(7)+",在表中所有>=7的数中,我离7最近");
        treeMap1.remove(5);
        System.out.println(treeMap1.get(5)+",删了就没有了哦");
        System.out.println("===========6==========");
    }
}

运行结果:

java 复制代码
true
false
true
cheng
null
null
===========1==========
true
false
false
===========2==========
true
true
1
1
true
true
2
2
false
false
===========3==========
true
false
A节点
null
true
true
A节点
B节点
===========4==========
错误信息:class class004.Code_HashAndTree$Node cannot be cast to class java.lang.Comparable (class004.Code_HashAndTree$Node is in unnamed module of loader 'app'; java.lang.Comparable is in module java.base of loader 'bootstrap')
这次节点都加入了
===========5==========
true
我是5
2,我最小
9,我最大
7,在表中所有<=8的数中,我离8最近
9,在表中所有>=8的数中,我离8最近
7,在表中所有<=7的数中,我离7最近
7,在表中所有>=7的数中,我离7最近
null,删了就没有了哦
===========6==========

4.单链表的节点结构

复制代码
Class Node<V>{
    V value;
    Node next;
}

由以上结构的节点依次连接起来所形成的链叫单链表结构。

双链表的节点结构

复制代码
Class Node<V>{
    V value;
    Node next;
    Node last;
}

由以上结构的节点依次连接起来所形成的链叫双链表结构。

单链表和双链表结构只需要给定一个头部节点head,就可以找到剩下的所有的节点。

5.反转单向和双向链表

【题目】分别实现反转单向链表和反转双向链表的函数

【要求】如果链表长度为N,时间复杂度要求为O(N),额外空间复杂度要求为O(1)

分析:反转单向链表的方法是逆序,node1->node2->node3,变成node3->node2-node1,双向链表同样如此。注意:如果反转操作会导致 head 改变,就需要把新的头节点返回 。如果不需要返回任何结果,可以定义成 void

流程图:

代码:

java 复制代码
package class004;

public class Code_ReverseList {
    //=======================
    //单向链表节点
    //=======================
    public static class Node{
        public int value;
        public Node next;
        public Node(int value){
            this.value=value;
        }
    }
    //=======================
    //双向链表节点
    //=======================
    public static class DoubleNode{
        public int value;
        public DoubleNode next;
        public DoubleNode last;

        public DoubleNode(int value){
            this.value=value;
        }
    }
    //=========================
    //反转单向链表
    //=========================
    public static Node reverseLinkedList(Node head){
        Node pre=null;
        Node next=null;
        while(head!=null){
            //保存原来的下一个节点
            next=head.next;
            //当前节点指向前一个节点
            head.next=pre;
            //pre向后移动
            pre=head;
            //head向后移动
            head=next;
        }
        //返回新的头节点
        return pre;
    }
    //=========================
    //反转双向链表
    //=========================
    public static DoubleNode reverseDoubleList(DoubleNode head){
        DoubleNode pre=null;
        DoubleNode next=null;
        while(head!=null){
            //保存原来的下一个节点
            next=head.next;
            //next反向
            head.next=pre;
            //last反向
            head.last=next;
            //pre向后移动
            pre=head;
            //head向后移动
            head=next;
        }
        //返回新的头节点
        return pre;
    }
    //打印单向链表
    public static void printLinkedList(Node head){
        while(head!=null){
            System.out.print(head.value+" ");
            head=head.next;
        }
        System.out.println();
    }
    //打印双向链表
    public static void printDoubleList(DoubleNode head){
        while(head!=null){
            System.out.print(head.value+" ");
            head=head.next;
        }
        System.out.println();
    }
    public static void main(String [] args){
        //========================
        //测试单向链表
        //========================
        Node node1=new Node(1);
        Node node2=new Node(2);
        Node node3=new Node(3);

        node1.next=node2;
        node2.next=node3;
        System.out.println("单链表反转前:");
        printLinkedList(node1);

        Node newHead=reverseLinkedList(node1);
        System.out.println("单链表反转后:");
        printLinkedList(newHead);

        //========================
        //测试双向链表
        //========================
        DoubleNode d1=new DoubleNode(1);
        DoubleNode d2=new DoubleNode(2);
        DoubleNode d3=new DoubleNode(3);

        d1.next=d2;
        d2.last=d1;
        d2.next=d3;
        d3.last=d2;

        System.out.println("双链表反转前: ");
        printDoubleList(d1);
        DoubleNode newDoubleHead=reverseDoubleList(d1);

        System.out.println("双链表反转后: ");
        printDoubleList(newDoubleHead);
    }
}

运行结果:

java 复制代码
单链表反转前:
1 2 3 
单链表反转后:
3 2 1 
双链表反转前: 
1 2 3 
双链表反转后: 
3 2 1 

6.打印两个有序链表的公共部分

【题目】给定两个有序链表的头指针head1和head2,打印两个链表的公共部分。

【要求】如果两个链表的长度之和为N,时间复杂度要求为O(N),额外空间复杂度要求为O(1)。

分析:

规则:a{i}和b{i}谁小谁先移动;相等时打印数字,打印完共同移动;越界后终止。

例如:{1->2->5-null}和{0->2->3->5-null},对应序列编号{1(a1)->2(a2)->5(a3)-null}和{0(b1)->2(b2)->3(b3)->5(b4)-null},分别设置两个指针p1,p2。初始化,p1指向a1,p2指向b1,a{i}和b{i}谁小谁先移动,由于a1>b1,p2右移动指向b2(2);接着,a1<b2,则p1向右移动指向a2(2);由于a2(2)=b2(2),打印数字2,且p1右移动指向a3(5),p2右移动指向b3(3);此时,由于b3<a3,p2右移动到b4(5);由于a3(5)=b4(5),打印数字5,且p1右移动指向null,p2右移动指向null,发生越界,终止。

代码:

java 复制代码
package class004;

public class Code_PrintCommonPart {
    //单链表节点
    public static class Node{
        public int value;
        public Node next;
        public Node(int value){
            this.value=value;
        }
    }
    //打印两个有序链表的公共部分
    public static void printCommonPart(Node head1,Node head2){
        Node p1=head1;
        Node p2=head2;
        //只要两个指针都没有越界,就继续比较
        while(p1!=null && p2!=null){
            //p1的值小,p1向后移动
            if(p1.value<p2.value){
                p1=p1.next;
            }
            //p2的值小,p2向右移动
            else if(p1.value>p2.value){
                p2=p2.next;
            }
            //两个值相等
            else {
                System.out.println(p1.value+" ");
                //两个指针同时向后移动
                p1=p1.next;
                p2=p2.next;
            }
        }
        System.out.println();
    }
    public static void main(String [] args){
        //链表1:
        //1->2->5->null
        Node a1=new Node(1);
        Node a2=new Node(2);
        Node a3=new Node(5);
        a1.next=a2;
        a2.next=a3;
        //链表2:
        //0->2->3->5_null
        Node b1=new Node(0);
        Node b2=new Node(2);
        Node b3=new Node(3);
        Node b4=new Node(5);

        b1.next=b2;
        b2.next=b3;
        b3.next=b4;

        System.out.println("链表1:1->2->5->null");
        System.out.println("链表2:0->2->3->5_null");
        System.out.println("两个链表的公共部分: ");
        printCommonPart(a1,b1);
    }
}

运行结果:

java 复制代码
链表1:1->2->5->null
链表2:0->2->3->5_null
两个链表的公共部分: 
2 
5 

7.面试时链表解题的方法论

1)对于笔试,不用太在乎空间复杂度,一切为了时间复杂度

2)对于面试,时间复杂度依然放在第一位,但是一定要找到空间最省的方法

重要技巧:

1)额外数据结构记录(哈希表等)

2)快慢指针

(三)面试题

1.判断一个链表是否为回文结构

【题目】给定一个单链表的头节点head,请判断该链表是否为回文结构。(leetcode234,剑指offer27)

【例子】

1->2->1,返回true;

1->2->2->1,返回true;

15->6->15,返回true;

1->2->3,返回false。

【要求】如果链表长度为N,时间复杂度达到O(N),额外空间复杂度达到O(1)。

分析:

(1)方法一,O(N)的空间复杂度,只用给定的单链表,采用"括号匹配"的方法,全部压入栈,弹出的时候依次对应

(2)方法二,O(N/2)的空间复杂度,加入"快慢指针",采用"括号匹配"的方法,将右半部分压入栈后弹出,左半部分依次比对,节省一半的栈空间。举例:对于一个链表,设置一个慢指针S,一次走一步,快指针F,一次走两步。当快指针走完的时候,慢指针走到终点的位置,这个时候,就可以把慢指针背后的结点放到栈里面去。细节处理:实现具体函数时,当链表结点是奇数时,慢指针要停在中点,当链表结点是偶数时,要确保慢指针停在对称轴左边第一个节点。特殊要求:如果测试用例非常小和刁钻,比如只有两个或三个节点,那么慢指针走到一半也许需要在第一个节点停下;题目条件可能会要求慢指针在中点前两个节点停下,例如{1,2,3,2,1}中S停在2处,{1,2,3,3,2,1}中停在2处。

(3)方法三(通常是面试时会考察的),O(1)的空间复杂度例,如链表{1->2->3->2->1},对应{a1,a2,a3,a4,a5},快慢指针S和F走完一轮后,慢指针S走到了3(a3)的位置并记录标志,快指针F走到了1(a5)位置,此时,将链表变成{1->2->3<-2<-1}。接着依次复制S走过的节点的值,让快指针F从a5位置往回走直到中点标志位(这里是a3)取走过结点的值进行对比。最后再返回true和false之前要把链表结构改回原样。

方法 思路 时间 额外空间
方法一 全部压栈 O(N) O(N)
方法二 快慢指针,只压一半 O(N) O(N/2),即O(N)
方法三 快慢指针 + 反转链表 O(N) O(1)
java 复制代码
方法一:

全链表
   ↓
全部压栈
   ↓
简单,但空间 O(N)


方法二:

快慢指针
   ↓
找到一半
   ↓
只压一半
   ↓
空间仍然 O(N),但省一半


方法三:

快慢指针
   ↓
找到中间
   ↓
原地反转后半部分
   ↓
两边比较
   ↓
恢复链表
   ↓
空间 O(1)

代码实现:

java 复制代码
package class004;

import java.util.Stack;

public class Code_IsPalindromeList {
    //单链表节点
    public static class Node{
        public int value;
        public Node next;
        public Node(int value){
            this.value=value;
        }
    }
    //=======================
    //方法一:全部节点压入栈
    //时间O(N)
    //空间O(N)
    public static boolean isPalindrome1(Node head){
        if(head==null || head.next==null){
            return true;
        }
        Stack<Node>stack=new Stack<>();
        Node cur=head;
        //所有节点压栈
        while(cur!=null){
            stack.push(cur);
            cur=cur.next;
        }
        //从头开始,与栈顶依次比较
        cur=head;
        while (cur!=null){
            if (cur.value!=stack.pop().value){
                return false;
            }
            cur=cur.next;
        }
        return true;
    }
    //===========================
    //方法二:快速指针+把右半部分压栈
    //时间O(N)
    //空间O(N/2)
    public static boolean isPalindrome2(Node head){
        if (head==null||head.next==null){
            return true;
        }
        Node slow=head;
        Node fast=head;
        /*
         * slow最终来到中间位置附近
         *
         * 奇数:
         * 1 2 3 2 1
         *     ↑
         *    slow
         *
         * 偶数:
         * 1 2 2 1
         *     ↑
         *    slow
         */
        while (fast!=null&&fast.next!=null){
            slow=slow.next;
            fast=fast.next.next;
        }
        /*
         * 如果fast != null:
         * 说明链表长度为奇数
         * slow此时位于真正的中点
         *
         * 中点不用比较,所以从slow.next开始
         *
         * 如果fast == null:
         * 说明链表长度为偶数
         * slow就是右半部分第一个节点
         */
        Node right;
        if (fast!=null){
            right=slow.next;
        }else {
            right=slow;
        }
        Stack<Node>stack=new Stack<>();
        //右半部分压栈
        while (right!=null){
            stack.push(right);
            right=right.next;
        }
        Node cur=head;
        //左半部分与栈比较
        while(!stack.isEmpty()){
            if(cur.value!=stack.pop().value){
                return false;
            }
            cur=cur.next;
        }
        return true;
    }
    //============================
    //方法三:
    //快慢指针+反转后半部分
    //时间O(N)
    //空间O(1)
    //============================
    public static boolean isPalindrome3(Node head){
        if (head==null||head.next==null){
            return true;
        }
        //slow:慢指针
        Node slow=head;
        //fast:快指针
        Node fast=head;
        /*
         * 找中点
         *
         * 奇数:
         *
         * 1 -> 2 -> 3 -> 2 -> 1
         *           ↑
         *          slow
         *
         * 偶数:
         *
         * 1 -> 2 -> 2 -> 1
         *      ↑
         *     slow
         *
         * 偶数时slow停在左中点
         */
        while (fast.next!=null&&fast.next.next!=null){
            slow=slow.next;
            fast=fast.next.next;
        }
        //===========================
        //开始反转slow后面的链表
        //===========================
        Node cur=slow.next;
        //暂时把左半部分断开
        slow.next=null;
        Node next=null;
        /*
         * 此时slow相当于反转过程中的pre
         *
         * 例如:
         *
         * 1 -> 2 -> 3 -> 2 -> 1
         *
         * slow = 3
         *
         * 最后得到:
         *
         * 1 -> 2 -> 3 <- 2 <- 1
         */
        while (cur!=null){
            next=cur.next;
            cur.next=slow;
            slow=cur;
            cur=next;
        }
        /*
         * slow现在位于链表最右端
         *
         * 保存它,因为之后还需要恢复链表
         */
        Node rightHead=slow;
        //==============================
        //从左右两边开始比较
        //==============================
        Node left=head;
        Node right=rightHead;
        boolean result=true;
        while (left!=null&&right!=null){
            if(left.value!=right.value){
                result=false;
                break;
            }
            left=left.next;
            right=right.next;
        }
        //=============================
        //恢复链表
        //=============================
        cur=rightHead.next;
        rightHead.next=null;
        while(cur!=null){
            next=cur.next;
            cur.next=rightHead;
            rightHead=cur;
            cur=next;
        }
        return result;
    }
    //=================================
    //打印链表
    //=================================
    public static void printList(Node head){
        while(head!=null){
            System.out.println(head.value);
            if(head.next!=null){
                System.out.print("->");
            }
            head=head.next;
        }
        System.out.println();
    }
    public static void main(String[] args) {

        // =========================
        // 测试1:5个节点
        // 1 -> 2 -> 3 -> 2 -> 1
        // 回文
        // =========================
        Node n1 = new Node(1);
        Node n2 = new Node(2);
        Node n3 = new Node(3);
        Node n4 = new Node(2);
        Node n5 = new Node(1);

        n1.next = n2;
        n2.next = n3;
        n3.next = n4;
        n4.next = n5;

        System.out.println("====== 5个节点测试 ======");
        System.out.println("原链表:");
        printList(n1);

        System.out.println("方法一:" + isPalindrome1(n1));
        System.out.println("方法二:" + isPalindrome2(n1));
        System.out.println("方法三:" + isPalindrome3(n1));

        System.out.println("方法三执行后的链表:");
        printList(n1);


        // =========================
        // 测试2:2个节点
        // 1 -> 1
        // 回文
        // =========================
        Node a1 = new Node(1);
        Node a2 = new Node(1);

        a1.next = a2;

        System.out.println("====== 2个节点测试1 ======");
        System.out.println("原链表:");
        printList(a1);

        System.out.println("方法一:" + isPalindrome1(a1));
        System.out.println("方法二:" + isPalindrome2(a1));
        System.out.println("方法三:" + isPalindrome3(a1));

        System.out.println("方法三执行后的链表:");
        printList(a1);


        // =========================
        // 测试3:2个节点
        // 1 -> 2
        // 不是回文
        // =========================
        Node b1 = new Node(1);
        Node b2 = new Node(2);

        b1.next = b2;

        System.out.println("====== 2个节点测试2 ======");
        System.out.println("原链表:");
        printList(b1);

        System.out.println("方法一:" + isPalindrome1(b1));
        System.out.println("方法二:" + isPalindrome2(b1));
        System.out.println("方法三:" + isPalindrome3(b1));

        System.out.println("方法三执行后的链表:");
        printList(b1);


        // =========================
        // 测试4:3个节点
        // 1 -> 2 -> 1
        // 回文
        // =========================
        Node c1 = new Node(1);
        Node c2 = new Node(2);
        Node c3 = new Node(1);

        c1.next = c2;
        c2.next = c3;

        System.out.println("====== 3个节点测试1 ======");
        System.out.println("原链表:");
        printList(c1);

        System.out.println("方法一:" + isPalindrome1(c1));
        System.out.println("方法二:" + isPalindrome2(c1));
        System.out.println("方法三:" + isPalindrome3(c1));

        System.out.println("方法三执行后的链表:");
        printList(c1);


        // =========================
        // 测试5:3个节点
        // 1 -> 2 -> 3
        // 不是回文
        // =========================
        Node d1 = new Node(1);
        Node d2 = new Node(2);
        Node d3 = new Node(3);

        d1.next = d2;
        d2.next = d3;

        System.out.println("====== 3个节点测试2 ======");
        System.out.println("原链表:");
        printList(d1);

        System.out.println("方法一:" + isPalindrome1(d1));
        System.out.println("方法二:" + isPalindrome2(d1));
        System.out.println("方法三:" + isPalindrome3(d1));

        System.out.println("方法三执行后的链表:");
        printList(d1);
    }
}

运行结果:

java 复制代码
====== 5个节点测试 ======
原链表:
1
->2
->3
->2
->1

方法一:true
方法二:true
方法三:true
方法三执行后的链表:
1
->2
->3
->2
->1

====== 2个节点测试1 ======
原链表:
1
->1

方法一:true
方法二:true
方法三:true
方法三执行后的链表:
1
->1

====== 2个节点测试2 ======
原链表:
1
->2

方法一:false
方法二:false
方法三:false
方法三执行后的链表:
1
->2

====== 3个节点测试1 ======
原链表:
1
->2
->1

方法一:true
方法二:true
方法三:true
方法三执行后的链表:
1
->2
->1

====== 3个节点测试2 ======
原链表:
1
->2
->3

方法一:false
方法二:false
方法三:false
方法三执行后的链表:
1
->2
->3

2.将单向链表按某值划分成左边小、中间相等、右边大的形式

【题目】给定一个单链表的头节点head,节点的值类型是整型,再给定一个整数pivot。实现一个调整链表的函数,将链表调整为左部分都是值小于pivot的节点,中间部分都是值等于pivot的节点,右部分都是值大于pivot的节点。

【进阶】在实现原问题功能的基础上增加如下的要求

【要求】调整后所有小于pivot的节点之间的相对顺序和调整前一样

【要求】调整后所有等于pivot的节点之间的相对顺序和调整前一样

【要求】调整后所有大于pivot的节点之间的相对顺序和调整前一样

【要求】时间复杂度请达到O(N),额外空间复杂度请达到O(1)。

(1)笔试方法:创建一个Node类型的数组,把单链表每个节点的值放到Node类型的数组中去,然后用partition,接着把每个node串起来。

(2)面试方法:首先初始化六个变量和一个指针,SH(小于部分的头)=null,ST(小于部分的尾)=null,EH(等于部分的头)=null,ET(等于部分的尾)=null,BH(大于部分的头)=null,BT(大于部分的尾)=null。以单链表{4->6->3->5->8->5->2}为例子:

|----|------|---------|--------------------------|---------|----------------------|---------|----------------------|
| 轮次 | 指针位置 | SH | ST | EH | ET | BH | BT |
| 0 | | SH=null | ST=null | EH=null | ET=null | BH=null | BT=null |
| 1 | 4 | SH=4 | ST=4 | EH=null | ET=null | BH=null | BT=null |
| 2 | 6 | SH=4 | ST=4 | EH=null | ET=null | BH=6 | BT=6 |
| 3 | 3 | SH=4 | ST=3(SH->ST){4->3} | EH=null | ET=null | BH=6 | BT=6 |
| 4 | 5 | SH=4 | ST=3(SH->ST){4->3} | EH=5 | ET=5 | BH=6 | BT=6 |
| 5 | 8 | SH=4 | ST=3(SH->ST){4->3} | EH=5 | ET=5 | BH=6 | BT=8(BH->BT){6->8} |
| 6 | 5 | SH=4 | ST=3(SH->ST){4->3} | EH=5 | ET=5(EH->ET){5->5} | BH=6 | BT=8(BH->BT){6->8} |
| 7 | 2 | SH=4 | ST=2(SH->ST){4->3->2} | EH=5 | ET=5(EH->ET){5->5} | BH=6 | BT=8(BH->BT){6->8} |

得到小于区域(SH->ST){4->3->2},等于区域(EH->ET){5->5},大于区域(BH->BT){6->8}。

最后,将小于区域的尾连等于区域的头,等于区域的尾连大于区域的头。注意:实际情况要判断这些区域是否存在值,讨论边界条件,否则空指针会报错。

代码:

java 复制代码
package class004;

public class Code_SmallerEqualBigger {
    public static class Node{
        public int value;
        public Node next;
        public Node(int data){
            this.value=data;
        }
    }
    public static Node listPartition1(Node head,int pivot){
        if(head==null){
            return head;
        }
        Node cur=head;
        int i=0;
        while (cur!=null){
            i++;
            cur=cur.next;
        }
        Node[] nodeArr=new Node[i];
        i=0;
        cur=head;
        for(i=0;i!=nodeArr.length;i++){
            nodeArr[i]=cur;
            cur=cur.next;
        }
        arrPartition(nodeArr,pivot);
        for(i=1;i!=nodeArr.length;i++){
            nodeArr[i-1].next=nodeArr[i];
        }
        nodeArr[i-1].next=null;
        return nodeArr[0];
    }

    public static void arrPartition(Node[] nodeArr,int pivot){
        int small=-1;
        int big=nodeArr.length;
        int index=0;
        while (index!=big){
            if(nodeArr[index].value<pivot){
                swap(nodeArr,++small,index++);
            }else if(nodeArr[index].value==pivot){
                index++;
            }else {
                swap(nodeArr,--big,index);
            }
        }
    }
    public static void swap(Node[] nodeArr,int a,int b){
        Node tmp=nodeArr[a];
        nodeArr[a]=nodeArr[b];
        nodeArr[b]=tmp;
    }

    public static Node listPartition2(Node head,int pivot){
        Node sH=null;//small head
        Node sT=null;//small tail
        Node eH=null;//equal head
        Node eT=null;//equal tail
        Node mH=null;//big head
        Node mT=null;//big tail
        Node next=null;//save next node
        //every node distributed to three lists
        while (head!=null){
            next=head.next;
            head.next=null;
            if(head.value<pivot){
                if(sH==null){
                    sH=head;
                    sT=head;
                }else {
                    sT.next=head;
                    sT=head;
                }
            }else if (head.value==pivot){
                if(eH==null){
                    eH=head;
                    eT=head;
                }else {
                    eT.next=head;
                    eT=head;
                }
            }else {
                if(mH==null){
                    mH=head;
                    mT=head;
                }else {
                    mT.next=head;
                    mT=head;
                }
            }
            head=next;
        }
        //small and equal reconnect
        if(sT !=null){//如果有小于区域
            sT.next=eH;
            eT=eT==null?sT:eT;//下一步,谁去连大于区域的头,谁就变成eT
        }
        //上面的if,不管跑了没有,et
        //all reconnect
        if (eT!=null){//如果小于区域和等于区域,不是都没有
            eT.next=mH;
        }
        return sH !=null?sH:(eH!=null?eH:mH);
    }
    public static void printLinkedList(Node node) {
        System.out.print("Linked List: ");
        while (node != null) {
            System.out.print(node.value);
            if (node.next != null) {
                System.out.print(" -> ");
            }
            // 非常重要
            node = node.next;
        }
        System.out.println();
    }
    // ========================================
    // 创建链表,方便测试
    // ========================================
    public static Node buildList(int[] arr) {
        if (arr == null || arr.length == 0) {
            return null;
        }
        Node head = new Node(arr[0]);
        Node cur = head;
        for (int i = 1; i < arr.length; i++) {

            cur.next = new Node(arr[i]);

            cur = cur.next;
        }
        return head;
    }

    public static void main(String[] args) {
        int pivot = 5;
        // ========================================
        // 测试1
        // 同时存在 <、=、> 三个区域
        // ========================================
        int[] arr1 = {4, 6, 3, 5, 8, 5, 2};
        Node head1 = buildList(arr1);
        System.out.println("====== 测试1:方法一 ======");
        System.out.println("pivot = " + pivot);
        System.out.println("调整前:");
        printLinkedList(head1);
        head1 = listPartition1(head1, pivot);
        System.out.println("调整后:");
        printLinkedList(head1);
        // ========================================
        // 方法二需要重新创建链表
        // 因为方法一已经改变了原链表结构
        // ========================================
        Node head2 = buildList(arr1);
        System.out.println();
        System.out.println("====== 测试1:方法二 ======");
        System.out.println("pivot = " + pivot);
        System.out.println("调整前:");
        printLinkedList(head2);
        head2 = listPartition2(head2, pivot);
        System.out.println("调整后:");
        printLinkedList(head2);
        // ========================================
        // 测试2:没有等于区域
        // ========================================
        int[] arr2 = {7, 3, 8, 2, 6, 1};
        Node head3 = buildList(arr2);
        System.out.println();
        System.out.println("====== 测试2:没有等于区域 ======");
        System.out.println("调整前:");
        printLinkedList(head3);
        head3 = listPartition2(head3, 5);
        System.out.println("调整后:");
        printLinkedList(head3);
        // ========================================
        // 测试3:只有小于区域
        // ========================================
        int[] arr3 = {1, 2, 3, 4};
        Node head4 = buildList(arr3);
        System.out.println();
        System.out.println("====== 测试3:只有小于区域 ======");
        System.out.println("调整前:");
        printLinkedList(head4);
        head4 = listPartition2(head4, 5);
        System.out.println("调整后:");
        printLinkedList(head4);
        // ========================================
        // 测试4:只有等于区域
        // ========================================
        int[] arr4 = {5, 5, 5, 5};
        Node head5 = buildList(arr4);
        System.out.println();
        System.out.println("====== 测试4:只有等于区域 ======");
        System.out.println("调整前:");
        printLinkedList(head5);
        head5 = listPartition2(head5, 5);
        System.out.println("调整后:");
        printLinkedList(head5);
        // ========================================
        // 测试5:只有大于区域
        // ========================================
        int[] arr5 = {8, 7, 9, 6};
        Node head6 = buildList(arr5);
        System.out.println();
        System.out.println("====== 测试5:只有大于区域 ======");
        System.out.println("调整前:");
        printLinkedList(head6);
        head6 = listPartition2(head6, 5);
        System.out.println("调整后:");
        printLinkedList(head6);
    }
}

运行结果:

java 复制代码
====== 测试1:方法一 ======
pivot = 5
调整前:
Linked List: 4 -> 6 -> 3 -> 5 -> 8 -> 5 -> 2
调整后:
Linked List: 4 -> 2 -> 3 -> 5 -> 5 -> 8 -> 6

====== 测试1:方法二 ======
pivot = 5
调整前:
Linked List: 4 -> 6 -> 3 -> 5 -> 8 -> 5 -> 2
调整后:
Linked List: 4 -> 3 -> 2 -> 5 -> 5 -> 6 -> 8

====== 测试2:没有等于区域 ======
调整前:
Linked List: 7 -> 3 -> 8 -> 2 -> 6 -> 1
调整后:
Linked List: 3 -> 2 -> 1 -> 7 -> 8 -> 6

====== 测试3:只有小于区域 ======
调整前:
Linked List: 1 -> 2 -> 3 -> 4
调整后:
Linked List: 1 -> 2 -> 3 -> 4

====== 测试4:只有等于区域 ======
调整前:
Linked List: 5 -> 5 -> 5 -> 5
调整后:
Linked List: 5 -> 5 -> 5 -> 5

====== 测试5:只有大于区域 ======
调整前:
Linked List: 8 -> 7 -> 9 -> 6
调整后:
Linked List: 8 -> 7 -> 9 -> 6

Process finished with exit code 0

通过测试用例可知:

方法 时间复杂度 额外空间 稳定性
listPartition1 O(N) O(N) 不稳定
listPartition2 O(N) O(1) 稳定

3.复制含有随机指针节点的链表

【题目】一种特殊的单链表节点类描述如下(leetcode138,剑指35)

复制代码
class Node {
    int value;
    Node next;
    Node rand;

    Node(int val) {
        value = val;
    }
}

rand指针是单链表节点结构中新增的指针,rand可能指向链表中的任意一个节点,也可能指向null。给定一个由Node节点类型组成的无环单链表的头节点head,请实现一个函数完成这个链表的复制,并返回复制的新链表的头节点。

【要求】时间复杂度O(N),额外空间复杂度O(1)。

分析:

(1)用hashmap的方法:

第一步,不考虑新链表中新指针怎么连的问题,采用map的方式,把老链表中的节点和指针拷贝出来,放到一个新链表中去,其中包括key(老节点),value(新节点)。

第一回遍历,克隆老节点对应新节点的关系,得到表格

|-------|--------|----------|
| map次数 | key(老) | value(新) |
| 1 | 1 | 1' |
| 2 | 2 | 2' |
| 3 | 3 | 3' |

第二回遍历,设置next和rand方向的指针。先确定next指针,{node1 next->node2},{node1' next->node2'},由表格可知{node2 next(map) node2'};接着确定rand方向的指针,{node1 rand->node3},{node1' rand->node3'},由表格可知{node3 map node3'},也就是说,来到每一个node i时,设置node的next和rand指针,最后把node i'返回。如图所示:

(2)不用hash表的写法:

首先构造:{node1 next->node1',node1' next->node2,node2 next->node2',node2' next->node3,node3 next->node3'},{node1 rand->node3},{node2 rand->node1},{node3 rand->null};接着遍历的时候一对一对拿,比如{node1 rand->node3},{node1' rand->node3'},必然可以得出{node3 next->node3'},这样每一个rand指针的克隆节点都可以设置好;最后在next指针的方向上把新老链表分离出来。核心思路:

java 复制代码
第一步:
老1 -> 新1 -> 老2 -> 新2 -> 老3 -> 新3

第二步:
通过 老rand.next
找到对应的新rand

第三步:
把老链表和新链表拆开

代码:

java 复制代码
package class004;

import java.util.HashMap;

public class Code_CopyListWithRandom {
    public static class Node{
        public int value;
        public Node next;
        public Node rand;
        public Node(int data){
            this.value=data;
        }
    }

    public  static Node copyListWithRand1(Node head){
        HashMap<Node,Node>map=new HashMap<Node,Node>();
        Node cur=head;
        while(cur!=null){
            //克隆节点挂到map里去
            map.put(cur,new Node(cur.value));
            cur=cur.next;
        }
        cur=head;
        while(cur!=null){
            //cur 老
            //map.get(cur)新,已经在上一个循环建立
            map.get(cur).next=map.get(cur.next);
            map.get(cur).rand=map.get(cur.rand);
            cur=cur.next;
        }
        return map.get(head);
    }

    public static Node copyListWithRand2(Node head){
        if(head==null){
            return null;
        }
        Node cur=head;
        Node next=null;
        //copy node and link to every node
        //1->2
        //1->1'->2
        while(cur!=null){
            //第一步,当前节点的下一个就放它的克隆节点,它的克隆节点就是next
            //克隆节点的再下一个,就是老的下一个
            next=cur.next;
            cur.next=new Node(cur.value);
            cur.next.next=next;
            cur=next;
        }
        cur=head;
        Node curCopy=null;
        //set copy node rand
        //1->1'->2->2'
        while (cur!=null){
            next=cur.next.next;
            curCopy=cur.next;
            //如果cur.rand不等于空,那么cur的克隆节点找到了,那么cur.rand的克隆节点是谁呢,
            //由于位置依赖,就是它的下一个节点;如果cur.rand等于空,那么cur的克隆节点就是空节点
            curCopy.rand=cur.rand !=null?cur.rand.next:null;
            cur=next;
        }
        Node res=head.next;
        cur=head;
        //split
        while(cur!=null){
            next=cur.next.next;
            curCopy=cur.next;
            cur.next=next;
            curCopy.next=next!=null?next.next:null;
            cur=next;
        }
        return res;
    }
    // ======================================
    // 打印链表
    //
    // 格式:
    //
    // 1(rand->3) -> 2(rand->1) -> 3(rand->null)
    // ======================================
    public static void printLinkedList(Node head) {
        while (head!=null){
            System.out.print(head.value);
            System.out.print("(rand->");
            if(head.rand!=null){
                System.out.print(head.rand.value);
            }else {
                System.out.print("null");
            }
            System.out.print(")");

            if(head.next!=null){
                System.out.print("->");
            }
            head=head.next;
        }
        System.out.println();
    }
    public static void main(String[] args) {
        // ======================================
        // 测试1:
        //
        // next:
        // 1 -> 2 -> 3
        //
        // rand:
        // 1 -> 3
        // 2 -> 1
        // 3 -> null
        // ======================================
        Node n1 = new Node(1);
        Node n2 = new Node(2);
        Node n3 = new Node(3);

        n1.next = n2;
        n2.next = n3;

        n1.rand = n3;
        n2.rand = n1;
        n3.rand = null;
        System.out.println("====== 原链表 ======");
        printLinkedList(n1);
        // ======================================
        // 测试方法一
        // ======================================
        Node copy1 = copyListWithRand1(n1);
        System.out.println();
        System.out.println("====== 方法一:HashMap复制 ======");
        printLinkedList(copy1);
        // ======================================
        // 测试方法二
        // ======================================
        Node copy2 = copyListWithRand2(n1);
        System.out.println();
        System.out.println("====== 方法二:O(1)额外空间复制 ======");
        printLinkedList(copy2);
        // ======================================
        // 检查原链表是否恢复
        // ======================================
        System.out.println();
        System.out.println("====== 方法二执行后的原链表 ======");
        printLinkedList(n1);
        // ======================================
        // 检查是否是真正的深拷贝
        // ======================================
        System.out.println();
        System.out.println("====== 检查是否为不同对象 ======");
        System.out.println("原head == 方法一copy?" + (n1 == copy1));
        System.out.println("原head == 方法二copy?" + (n1 == copy2));
        // ======================================
        // 测试2:只有一个节点
        //
        // 5.rand -> 5
        // ======================================
        Node single = new Node(5);
        single.rand = single;
        System.out.println();
        System.out.println("====== 单节点测试 ======");
        System.out.println("原链表:");
        printLinkedList(single);
        Node singleCopy = copyListWithRand2(single);
        System.out.println("复制链表:");
        printLinkedList(singleCopy);
        // ======================================
        // 测试3:空链表
        // ======================================
        System.out.println();
        System.out.println("====== 空链表测试 ======");
        Node nullCopy = copyListWithRand2(null);
        System.out.println(nullCopy == null ? "复制结果:null" : "复制失败");
    }
}

运行结果:

java 复制代码
====== 原链表 ======
1(rand->3)->2(rand->1)->3(rand->null)

====== 方法一:HashMap复制 ======
1(rand->3)->2(rand->1)->3(rand->null)

====== 方法二:O(1)额外空间复制 ======
1(rand->3)->2(rand->1)->3(rand->null)

====== 方法二执行后的原链表 ======
1(rand->3)->2(rand->1)->3(rand->null)

====== 检查是否为不同对象 ======
原head == 方法一copy?false
原head == 方法二copy?false

====== 单节点测试 ======
原链表:
5(rand->5)
复制链表:
5(rand->5)

====== 空链表测试 ======
复制结果:null
方法 核心思路 时间 额外空间
HashMap 老节点 -> 新节点 映射 O(N) O(N)
穿插复制 老节点.next = 新节点 代替Map O(N) O(1)

4.两个单链表相交的一系列问题

leetcode160

【题目】给定两个可能有环也可能无环的单链表,头节点head1和head2。请实现一个函数,如果两个链表相交,请返回相交的第一个节点。如果不相交,返回null。

【要求】如果两个链表长度之和为N,时间复杂度请达到O(N),额外空间复杂度请达到O(1)。

分析:

首先明白相交是什么意思,比如第一个链表的序列是{a1->a2->a3->a4},第二个链表的序列是{b1->b2->b3},其中a3和b2是同一个节点(内存地址相同,而不是值相同),那么{a3,b2}称为第一个相交节点;接着判断一个链表有环和无环是什么意思,对于一个链表序列{a1->a2->a3->a4->a5->a3},那么a3是第一个入环节点,{a3->a4->a5->a3}构成一个环。

说明由于单链表的设定(只有一个next指针),只能出现有环的{a->b->c->d->e->c}或无环的结构{a->b->c->d->e},不可能出现{a->b->c->d->e->c->f->g}的结构,这样就有两个next指针了,所以不行。

思路1,hashmap实现:

例如,一个有环单链表{a->b->c->d->e->c},设定一个集合Set,初始化为空集合

|------|------|-------------|---------|-------------|
| 执行次数 | 指针位置 | 元素i是否在set内? | 操作 | Set |
| 0 | null | | 无 | {} |
| 1 | a | 不在 | a入Set | {a} |
| 2 | b | 不在 | b入Set | {a,b} |
| 3 | c | 不在 | c入Set | {a,b,c} |
| 4 | d | 不在 | d入Set | {a,b,c,d} |
| 5 | e | 不在 | e入Set | {a,b,c,d,e} |
| 6 | c | 在 | c是第一个节点 | |

具体可以采用快慢指针实现。对于链表{a->b->c->d->e->f->g->h->I->f},环是{f->g->h->I->f},设置快指针F和慢指针S,F和S的起点都在a。首先,如果F走到最后发现是空节点,那么这个链表肯定没有环。如果这个链表有环的话,快指针和慢指针肯定会在这个环的节点相遇。

第一轮:

|----|--------|--------|----------------|----------------|----------|
| 轮次 | 慢指针S位置 | 快指针F位置 | next指针方向节点>2? | 快指针F是否回越过相交节点? | F和S是否相遇? |
| 0 | a | a | 是 | 否 | |
| 1 | b | c | 是 | 否 | |
| 2 | c | e | 是 | 否 | |
| 3 | d | g | 是 | 否 | |
| 4 | e | I | 否 | 否 | |
| 5 | f | g | 是 | 是 | |
| 6 | g | I | 否 | 否 | |
| 7 | h | g | 是 | 是 | |
| 8 | I | I | 否 | 否 | 相遇 |

这个时候,令快指针F回到开头a,慢指针留在原地I。接下来,规定每个指针一次都只能走一步,两个指针同时按照next方向走,快指针的路线{a->b->c->d->e->f},走了五步,到达f;慢指针的路线{I->f->g->h->I->f},走了五步。于是快指针和慢指针再次相遇,不过这个相遇节点是环的开始节点。

结论:m是外节点数,n是环节点数,S一共走了2m+n-n%m步,在环上走了m+n-n%m步,一定是n的倍数,一定回到环的头。证明略,见leetcode142,leetcode LCR023。

接下来,就可以讨论两个链表相交的问题了。

head1一次循环后,得到第一个入环节点是loop1;head2一次循环后,得到第一个入环节点是loop2。

(1)loop1==null,loop2==null,有两种情况:

(a)如果两个单链表不相交的话,图像是两条平行线段。

(b)如果两个单链表相交的话,图像是交汇的Y型结构,Y型的下半部分都共有。

这两种情况下,先从head1开始遍历单链表1的各个节点,直到最后一个节点,记为end1,同时记录链表长度为len1;先从head2开始遍历单链表2的各个节点,直到最后一个节点,记为end2,同时记录链表长度为len2。此时,判断end1和end2节点的内存地址是不是同一个,如果不是,那么链表1和链表2不可能相交;如果是,那么链表1和链表2存在相交节点(Y型的下半部分),此时比较len1和len2的差值,记为d,假设len1大于len2,那么让链表1的指针先走d步,接着链表2的指针开始走,最后这两个指针必然能够在相交节点相遇。

(2)loop1和loop2一个为空,一个不为空,这种情况,链表1和链表2不可能相交

(3)loop1和loop2都有环,有三种情况,如图所示

(a)如果两个单链表不相交的话,图像是两个6型图案。

(b)如果两个单链表相交的话,图像是交汇的Y型结构且Y型的下半部分呈现一个6型,且入环节点相同。

(c)如果两个单链表相交的话,图像是交汇的Y型结构且Y型的下半部分呈现一个6型,且入环节点不相同,类似于天线宝宝的头。

情况区分思路:情况(a),链表走到结尾后,如同情况(1)(a)那样讨论loop1和loop2的内存地址是否相同,复用代码思路。情况(b),链表走到有环交汇节点时,将图像切割成Y型和O型两个部分,像(1)(b)那样讨论Y型结构。情况(a)和(c)的区分,流程是让loop1继续往下走,如果在转回自己的过程中能够遇到loop2,就是情况(c),此时返回两个相交节点;如果在转回自己的过程中没遇到loop2,就是情况(a),此时没有相交节点。

综合判断流程:

java 复制代码
                head1、head2
                     |
              分别找入环节点
                     |
               loop1、loop2
                     |
        ┌────────────┼────────────┐
        ↓            ↓            ↓
      都null       一个null      都不null
        |            |            |
      都无环       不可能相交      都有环
        |                         |
     noLoop()               loop1 == loop2 ?
                               /       \
                              是        否
                              |          |
                      环外按无环处理    loop1绕环
                              |          |
                       找第一个交点    能遇到loop2?
                                         /  \
                                        是   否
                                        |     |
                                      相交   不相交

实现代码:

java 复制代码
package class004;

public class Code_FindFirstIntersectNode {
    public static class Node{
        public int value;
        public Node next;
        public Node(int data){
            this.value=data;
        }
    }
    public static Node getIntersectNode(Node head1,Node head2){
        if(head1==null||head2==null){
            return null;
        }
        Node loop1=getLoopNode(head1);
        Node loop2=getLoopNode(head2);
        if(loop1==null&&loop2==null){
            return noLoop(head1,head2);
        }
        if(loop1!=null&&loop2!=null){
            return bothLoop(head1,loop1,head2,loop2);
        }
        return null;
    }

    //找到链表第一个入环节点,如果无环,返回null
    public static Node getLoopNode(Node head){
        if (head==null||head.next==null||head.next.next==null){
            return null;
        }
        Node n1=head.next;//n1->slow
        Node n2=head.next.next;//n2->fast
        while(n1!=n2){
            if(n2.next==null||n2.next.next==null){
                return null;
            }
            n2=n2.next.next;
            n1=n1.next;
        }
        n2=head;//n2->walk again from head
        while(n1!=n2){
            n1=n1.next;
            n2=n2.next;
        }
        return n1;
    }
    //如果两个链表都无环,返回第一个相交节点,如果不相交,返回null
    public static Node noLoop(Node head1,Node head2){
        if (head1==null||head2==null){
            return null;
        }
        Node cur1=head1;
        Node cur2=head2;
        int n=0;//两个长度变量优化成一个来做,n++和n--叠加就是长度差值
        while(cur1.next!=null){
            n++;
            cur1=cur1.next;
        }
        while(cur2.next!=null){
            n--;
            cur2=cur2.next;
        }
        //判断内存地址是否相同
        if(cur1!=cur2){
            return null;
        }
        //内存地址交换,进行重定位
        cur1=n>0?head1:head2;//谁长,谁的头变成cur1
        cur2=cur1==head1?head2:head1;//谁短谁的头变成cur2
        n=Math.abs(n);
        //长链表先走差值部分,短链表再走
        while (n!=0){
            n--;
            cur1=cur1.next;
        }
        while (cur1!=cur2){
            cur1=cur1.next;
            cur2=cur2.next;
        }
        //第一个相交的节点
        return cur1;
    }
    //两个有环链表,返回第一个相交节点,如果不相交返回null
    public static Node bothLoop(Node head1,Node loop1,Node head2,Node loop2){
        Node cur1=null;
        Node cur2=null;
        //情况二
        if(loop1==loop2){
            cur1=head1;
            cur2=head2;
            int n=0;
            while (cur1!=loop1){
                n++;
                cur1=cur1.next;
            }
            while (cur2!=loop2){
                n--;
                cur2=cur2.next;
            }
            //重定位
            cur1=n>0?head1:head2;
            cur2=cur1==head1?head2:head1;
            n=Math.abs(n);
            while (n!=0){
                n--;
                cur1=cur1.next;
            }
            while (cur1!=cur2){
                cur1=cur1.next;
                cur2=cur2.next;
            }
            return cur1;
        }else {
            cur1=loop1.next;
            while (cur1!=loop1){
                if(cur1==loop2){
                    return loop1;
                }
                cur1=cur1.next;
            }
            return null;
        }
    }
    public static void main(String[] args) {

        // =====================================================
        // 测试1:两个无环链表相交
        //
        // head1:
        // 1 -> 2 -> 3 \
        //               6 -> 7
        // 4 -> 5 ------/
        // head2:
        //
        // 第一个相交节点:6
        // =====================================================

        Node common1 = new Node(6);
        Node common2 = new Node(7);

        common1.next = common2;

        Node a1 = new Node(1);
        Node a2 = new Node(2);
        Node a3 = new Node(3);

        a1.next = a2;
        a2.next = a3;
        a3.next = common1;

        Node b1 = new Node(4);
        Node b2 = new Node(5);

        b1.next = b2;
        b2.next = common1;

        System.out.println("====== 测试1:两个无环链表相交 ======");

        Node ans1 = getIntersectNode(a1, b1);

        System.out.println(
                ans1 != null
                        ? "第一个相交节点:" + ans1.value
                        : "不相交"
        );


        // =====================================================
        // 测试2:两个无环链表不相交
        //
        // 1 -> 2 -> 3
        //
        // 4 -> 5 -> 6
        //
        // 结果:null
        // =====================================================

        Node c1 = new Node(1);
        Node c2 = new Node(2);
        Node c3 = new Node(3);

        c1.next = c2;
        c2.next = c3;

        Node d1 = new Node(4);
        Node d2 = new Node(5);
        Node d3 = new Node(6);

        d1.next = d2;
        d2.next = d3;

        System.out.println();
        System.out.println("====== 测试2:两个无环链表不相交 ======");

        Node ans2 = getIntersectNode(c1, d1);

        System.out.println(
                ans2 != null
                        ? "第一个相交节点:" + ans2.value
                        : "不相交"
        );


        // =====================================================
        // 测试3:一个有环,一个无环
        //
        // 无环:
        // 1 -> 2 -> 3
        //
        // 有环:
        // 4 -> 5 -> 6
        //      ↑    |
        //      └────┘
        //
        // 不可能相交
        // =====================================================

        Node e1 = new Node(1);
        Node e2 = new Node(2);
        Node e3 = new Node(3);

        e1.next = e2;
        e2.next = e3;

        Node f1 = new Node(4);
        Node f2 = new Node(5);
        Node f3 = new Node(6);

        f1.next = f2;
        f2.next = f3;
        f3.next = f2;

        System.out.println();
        System.out.println("====== 测试3:一个有环,一个无环 ======");

        Node ans3 = getIntersectNode(e1, f1);

        System.out.println(
                ans3 != null
                        ? "第一个相交节点:" + ans3.value
                        : "不相交"
        );


        // =====================================================
        // 测试4:两个有环链表
        // 入环节点相同,并且在入环之前已经相交
        //
        // head1:
        // 1 -> 2 \
        //          5 -> 6 -> 7 -> 8
        // 3 -> 4 /      ↑         |
        // head2:         └─────────┘
        //
        // 第一个相交节点:5
        // 入环节点:6
        // =====================================================

        Node g5 = new Node(5);
        Node g6 = new Node(6);
        Node g7 = new Node(7);
        Node g8 = new Node(8);

        g5.next = g6;
        g6.next = g7;
        g7.next = g8;
        g8.next = g6; // 入环点6

        Node g1 = new Node(1);
        Node g2 = new Node(2);

        g1.next = g2;
        g2.next = g5;

        Node h1 = new Node(3);
        Node h2 = new Node(4);

        h1.next = h2;
        h2.next = g5;

        System.out.println();
        System.out.println("====== 测试4:同一入环点,环前相交 ======");

        System.out.println(
                "head1入环节点:" + getLoopNode(g1).value
        );

        System.out.println(
                "head2入环节点:" + getLoopNode(h1).value
        );

        Node ans4 = getIntersectNode(g1, h1);

        System.out.println(
                ans4 != null
                        ? "第一个相交节点:" + ans4.value
                        : "不相交"
        );


        // =====================================================
        // 测试5:两个有环链表
        // 入环节点相同,在入环节点才第一次相交
        //
        // 1 -> 2 ----\
        //              6 -> 7 -> 8
        // 3 -> 4 -> 5 /
        //              ↑         |
        //              └─────────┘
        //
        // 第一个相交节点:6
        // =====================================================

        Node loop6 = new Node(6);
        Node loop7 = new Node(7);
        Node loop8 = new Node(8);

        loop6.next = loop7;
        loop7.next = loop8;
        loop8.next = loop6;

        Node i1 = new Node(1);
        Node i2 = new Node(2);

        i1.next = i2;
        i2.next = loop6;

        Node j1 = new Node(3);
        Node j2 = new Node(4);
        Node j3 = new Node(5);

        j1.next = j2;
        j2.next = j3;
        j3.next = loop6;

        System.out.println();
        System.out.println("====== 测试5:同一入环点,在入环点相交 ======");

        Node ans5 = getIntersectNode(i1, j1);

        System.out.println(
                ans5 != null
                        ? "第一个相交节点:" + ans5.value
                        : "不相交"
        );


        // =====================================================
        // 测试6:两个有环链表
        // 属于同一个环,但是入环节点不同
        //
        //             6 -> 7
        //            ↑     ↓
        // head1 -> 6       8
        //            ↑     ↓
        //            9 <- 8
        //
        // head1从6入环
        // head2从8入环
        //
        // loop1 != loop2
        // 但是两个入口属于同一个环
        //
        // 返回loop1或loop2都可以
        // 当前代码返回loop1
        // =====================================================

        Node k6 = new Node(6);
        Node k7 = new Node(7);
        Node k8 = new Node(8);
        Node k9 = new Node(9);

        k6.next = k7;
        k7.next = k8;
        k8.next = k9;
        k9.next = k6;

        Node k1 = new Node(1);
        Node k2 = new Node(2);

        k1.next = k2;
        k2.next = k6; // head1从6入环

        Node l1 = new Node(3);
        Node l2 = new Node(4);

        l1.next = l2;
        l2.next = k8; // head2从8入环

        System.out.println();
        System.out.println("====== 测试6:同一环,不同入环节点 ======");

        Node loopNode1 = getLoopNode(k1);
        Node loopNode2 = getLoopNode(l1);

        System.out.println(
                "head1入环节点:" + loopNode1.value
        );

        System.out.println(
                "head2入环节点:" + loopNode2.value
        );

        Node ans6 = getIntersectNode(k1, l1);

        System.out.println(
                ans6 != null
                        ? "返回的相交节点:" + ans6.value
                        : "不相交"
        );


        // =====================================================
        // 测试7:两个有环链表,但属于两个独立的环
        //
        // 链表1:
        // 1 -> 2 -> 3 -> 4
        //           ↑      |
        //           └──────┘
        //
        // 链表2:
        // 5 -> 6 -> 7 -> 8
        //           ↑      |
        //           └──────┘
        //
        // 结果:不相交
        // =====================================================

        Node m1 = new Node(1);
        Node m2 = new Node(2);
        Node m3 = new Node(3);
        Node m4 = new Node(4);

        m1.next = m2;
        m2.next = m3;
        m3.next = m4;
        m4.next = m3;

        Node n1 = new Node(5);
        Node n2 = new Node(6);
        Node n3 = new Node(7);
        Node n4 = new Node(8);

        n1.next = n2;
        n2.next = n3;
        n3.next = n4;
        n4.next = n3;

        System.out.println();
        System.out.println("====== 测试7:两个独立的环 ======");

        Node ans7 = getIntersectNode(m1, n1);

        System.out.println(
                ans7 != null
                        ? "第一个相交节点:" + ans7.value
                        : "不相交"
        );


        // =====================================================
        // 测试8:空链表
        // =====================================================

        System.out.println();
        System.out.println("====== 测试8:空链表 ======");

        Node ans8 = getIntersectNode(null, a1);

        System.out.println(
                ans8 == null
                        ? "结果:null"
                        : "错误"
        );
    }
}

运行结果:

java 复制代码
====== 测试1:两个无环链表相交 ======
第一个相交节点:6

====== 测试2:两个无环链表不相交 ======
不相交

====== 测试3:一个有环,一个无环 ======
不相交

====== 测试4:同一入环点,环前相交 ======
head1入环节点:6
head2入环节点:6
第一个相交节点:5

====== 测试5:同一入环点,在入环点相交 ======
第一个相交节点:6

====== 测试6:同一环,不同入环节点 ======
head1入环节点:6
head2入环节点:8
返回的相交节点:6

====== 测试7:两个独立的环 ======
不相交

====== 测试8:空链表 ======
结果:null
相关推荐
LayZhangStrive2 小时前
数据结构与算法 - 堆排序
java·数据结构·算法·排序算法·堆排序·大根堆
a187927218313 小时前
【算法】动态规划第五篇:区间 DP——从“看什么都像背包“到“最后戳谁“
算法·leetcode·动态规划·dp·区间·区间dp·算法讲解
wuyk5553 小时前
13.堆排序:基于完全二叉树的高效排序算法一、什么是堆排序?
开发语言·算法·排序算法
rannn_1114 小时前
【力扣hot100】多维动态规划|62、64、5、1143、72
算法·leetcode·动态规划
find1star13 小时前
LeetCode 141:环形链表
java·算法·leetcode·链表
LB211213 小时前
力扣160 21 86
算法·leetcode·职场和发展
heima201614 小时前
长期复盘:餐饮店活动拓客链接的行业现状与使用困境
链表
a1879272183115 小时前
【算法】动态规划第二篇:双序列 DP 三课——继承、计步与断链
算法·leetcode·动态规划·dp·回溯·暴力·算法讲解
青山木17 小时前
Hot 100 --- 数组中的第K个最大元素
java·数据结构·算法·排序算法