Bootstrap

【数据结构】(6) LinkedList 链表

一、什么是链表

1、链表与顺序表对比

不同点LinkedListArrayList
物理存储上不连续连续
随机访问效率O(N)O(1)
插入、删除效率O(1)O(N)

3、链表的分类

        链表根据结构分类,可分为单向/双向无头结点/有头节点非循环/循环链表,这三组每组各取一个就构成一种结构链表。其中,单向、不带头、非循环链表学习的重点双向、不带头、非循环链表在实际开发中常用。

        以单向、不带头、非循环链表为例:

        双向、不带头、非循环链表:

        单向、带头、非循环链表(好处在于,不用特别处理头节点,比如增删时):

        单向、不带头、循环链表:

二、单向、不带头、非循环链表的实现

1、IList 接口

        定义 IList 接口,声明线性表需要实现的方法:

package listinterface;

public interface IList {
    //头插法
    void addFirst(int data);
    //尾插法
    void addLast(int data);
    //任意位置插入,第一个数据节点为0号下标
    void addIndex(int index,int data);
    //查找是否包含关键字key是否在单链表当中
    boolean contains(int key);
    //删除第一次出现关键字为key的节点
    void remove(int key);
    //删除所有值为key的节点
    void removeAllKey(int key);
    //得到单链表的长度
    int size();
    void clear();
    void display();
}

2、结点内部类

        结点仅由链表内部使用,且每个结点的产生不依赖于某个链表,可定义为链表的私有静态内部类:

public class MySingleList implements IList {
    // 结点内部类
    private static class Node {
        int data;
        Node next;

        public Node(int data) {
            this.data = data;
        }
    }
    // 头结点
    private Node head;
    // 链表长度
    private int size;

    ............
}

3、打印链表

    @Override
    public void display() {
        Node cur = head;
        while (cur != null) {
            System.out.print(cur.data + " ");
            cur = cur.next;
        }
        System.out.println(" size: " + size);
    }

4、清空链表、获取链表长度

    @Override
    public int size() {
        return size;
    }

    @Override
    public void clear() {
        head = null;
        size = 0;
    }

        head 为空,没有引用指向 node1,自动回收 node1;node1 回收,没有引用指向 node2,自动回收 node2……故 clear 只需 head 设为 null 即可。

5、头插法

    @Override
    public void addFirst(int data) {
        Node newNode = new Node(data);
        newNode.next = head;
        head = newNode;
//        if (head != null) {
//            newNode.next = head;
//        }
//        head = newNode;
        size++;
    }

6、尾插法

    @Override
    public void addLast(int data) {
        Node newNode = new Node(data);
        if (head == null) {
            head = newNode;
            size++;
            return;
        }
        Node cur = head;
        while (cur.next != null) { // 找到尾节点
            cur = cur.next;
        }
        cur.next = newNode;
        size++;
    }

7、任意位置插入

受查异常,父类方法没抛出异常,子类方法不能抛出异常;父类抛出了,子类不做要求。非受查异常,不做要求。)为了不改动 IList,且让 addIndex 抛出异常,以便在 main 处对异常做处理,链表下标越界异常选择继承非受查异常 RuntimeExcption。

package myexception;

public class ListIndexOutOfBoundsException extends RuntimeException {
    public ListIndexOutOfBoundsException(String message) {
        super(message);
    }
}
    private void checkIndexOutOfBounds(int index) throws ListIndexOutOfBoundsException {
        if (index < 0 || index > size) {
            throw new ListIndexOutOfBoundsException("插入位置不合法。" + "Index: " + index + ", Size: " + size);
        }
    }

    private Node findIndex(int index) {
        Node cur = head;
        int cnt = 0;
        while (cnt != index) {
            cur = cur.next;
            cnt++;
        }
        return cur;
    }

    @Override
    public void addIndex(int index, int data) throws ListIndexOutOfBoundsException {
        // 先检查索引是否越界
        checkIndexOutOfBounds(index);
        if (index == 0) { // 在 0 处插入,没有前驱节点
            addFirst(data);
            return;
        }
        if (index == size) { // 在末尾插入
            addLast(data);
            return;
        }
        Node newNode = new Node(data);
        // 找到插入位置前的节点
        Node pre = findIndex(index - 1);
        // 插入新节点
        newNode.next = pre.next;
        pre.next = newNode;
        size++;
    }

8、删除第一次出现关键字为key的节点

    private Node findKey(int key) {
        Node cur = head;
        while (cur.next != null) {
            if (cur.next.data == key) {
                return cur;
            }
            cur = cur.next;
        }
        return null;
    }

    @Override
    public void remove(int key) {
        if (head == null) { // 空链表
            return;
        }
        if (head.data == key) { // 头节点就是要删除的节点
            head = head.next;
            size--;
            return;
        }
        Node pre = findKey(key);
        if (pre != null) { // 找到了要删除的节点
            pre.next = pre.next.next;
            size--;
        } else { // 要删除的节点不存在
            System.out.println("要删除的节点不存在。");
        }
    }

9、删除所有关键字为 key 的节点

    @Override
    public void removeAllKey(int key) {
        if (head == null) { // 空链表
            return;
        }
        Node pre = head;
        Node cur = head.next;
        while (cur != null) {
            if(cur.data == key) {
                pre.next = cur.next;
                // cur = cur.next;
            }
            else {
                pre = cur;
                // cur = cur.next;
            }
            cur = cur.next;
        }
        if (head.data == key) {
            head = head.next;
            size--;
        }
    }

三、链表面试题练习

1、反转链表

206. 反转链表 - 力扣(LeetCode)

思路:从头开始,边遍历边反转,最后尾结点为 head。

class Solution {
    public ListNode reverseList(ListNode head) {
        // 特殊处理 空链表、只有一个结点的链表
        if(head == null || head.next == null) { // 两者顺序不能反,因为 null 没有next
            return head;
        }
        ListNode cur = head.next;
        // 头结点,其 next 为 null 
        head.next = null;
        // 边反转边遍历,直到反转完尾结点
        while(cur != null) {
            ListNode curN = cur.next; 
            cur.next = head;
            head = cur;
            cur = curN;
        }
        return head;
    }
}

2、链表的中间结点

876. 链表的中间结点 - 力扣(LeetCode)

思路

1、计算链表长度的一半,再走一半长。时间复杂度 O(N) + O(N/2)。

2、设置 slow 和 fast 从 head 开始走,slow 每次走 1 步,fast 每次走 2 步。那么 fast 走完全程(fast = null 或者 fast.next = null),必是 slow 的两倍长度,故 slow 就是中间节点的位置。时间复杂度 O(N/2) ,更优。

class Solution {
    public ListNode middleNode(ListNode head) { // 根据题目,链表非空
        // 从头节点开始
        ListNode slow = head;
        ListNode fast = head;
        // fast 走完整个链表
        while(fast != null && fast.next != null) {
            // slow 每次走 1 步,fast 每次走 2 步
            slow = slow.next;
            fast = fast.next.next;
        }
        // slow 就是中间结点
        return slow;
    }
}

3、返回倒数第 k 个结点

面试题 02.02. 返回倒数第 k 个节点 - 力扣(LeetCode)

思路:slow 从 head 开始,fast 比 slow 先多走 k-1 步,然后 slow、fast 每次只走一步,直到 fast 走到尾结点。

class Solution {
    public int kthToLast(ListNode head, int k) {
        // 根据题目,k 保证有效,因此不用判断 k<=0 和 k > len 的情况
        ListNode slow = head;
        ListNode fast = head;
        // 当 fast 先走 k-1 步
        int cnt = k-1;
        // k保证有效,fast 不会走过头
        while(cnt != 0) { 
            fast = fast.next;
            cnt--;
        }
        // 让 fast 走到尾结点,slow 就是倒数第 k 个结点
        while(fast.next != null) {
            slow = slow.next;
            fast = fast.next;
        }
        return slow.val;
    }
}

 4、合并两个有序列表

21. 合并两个有序链表 - 力扣(LeetCode)

思路:分别同时遍历两个链表,更小的插入新链表,插入的链表插入后更新为 next,直到 l1 或者 l2 遍历完了。没遍历完的那个链表,把剩的接在新链表尾。为了不单独处理头指针,新链表带头节点。

class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        ListNode head = new ListNode(); // 创建头节点
        ListNode cur = head;
        ListNode cur1 = list1;
        ListNode cur2 = list2;
        // 其中一个遍历完就退出
        while(cur1 != null && cur2 != null) {
            if(cur1.val < cur2.val) {
                cur.next = cur1;
                // cur = cur.next;
                cur1 = cur1.next;
            }
            else {
                cur.next = cur2;
                // cur = cur.next;
                cur2 = cur2.next;
            }
            cur = cur.next;
        }
        // 没遍历完的,接在后面
        if(cur1 != null) {
            cur.next = cur1;
        }
        if(cur2 != null) {
            cur.next = cur2;
        }
        return head.next; // 不带头节点
    }
}

5、链表分割

链表分割_牛客题霸_牛客网

思路:先创建两个链表,依次遍历原链表,小于和大于等于 x 的分开尾插入两个链表,再合并。

public class Partition {
    public ListNode partition(ListNode pHead, int x) {
        // 创建两个头节点
        ListNode head1 = new ListNode(-1); 
        ListNode head2 = new ListNode(-1);
        // 遍历原链表,每个结点与 x 比大小,分类放在两个新链表中
        ListNode cur1 = head1;
        ListNode cur2 = head2; 
        while(pHead != null) {
            if(pHead.val < x) {
                cur1.next = pHead;
                cur1 = cur1.next;
            }
            else {
                cur2.next = pHead;
                cur2 = cur2.next;
            }
            pHead = pHead.next;
        }
        // 现在 cur1、cur2 分别指向两个链表的尾结点。
        cur1.next = head2.next; // 链表1的尾接上链表2的头
        cur2.next = null; // 链表2的尾接上 null
        // 如果最后链表1为空,cur1就是head1,没问题
        // 如果最后链表2为空,cur2就是head2,没问题
        return head1.next;
    }
}

6、链表的回文结构

链表的回文结构_牛客题霸_牛客网

思路

1、求尾结点,头、尾同时前进并比较,到 head = tail(奇数长度) 或者 head.next = tail (偶数长度)为止。tail 往前走行不通,因为是单链表。

2、把原链表的后一半反转,再将反转链表与原链表对比,直到反转链表遍历完。时间复杂度O(N/2)+O(N/2)+O(N/2)=O(N)。找到一半处、将一半反转、对比一半。

public class PalindromeList {

    private ListNode findHalf(ListNode head) {
        ListNode slow = head;
        ListNode fast = head;
        while(fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        return slow;
    }

    private ListNode reverse(ListNode head) {
        if(head == null || head.next == null) {
            return head;
        }
        ListNode cur = head.next;
        head.next = null;
        while(cur != null) {
            ListNode curN = cur.next;
            cur.next = head;
            head = cur;
            cur = curN;
        }
        return head;
    }

    public boolean chkPalindrome(ListNode A) {
        // 找到中间结点
        ListNode halfNode = findHalf(A);
        // 将后一半反转
        ListNode B = reverse(halfNode);
        // 将后半段与前半段对比,后半段遍历完退出
        while(B != null) {
            if(A.val != B.val) {
                return false;
            }
            A = A.next;
            B = B.next;
        }
        return true;
    }
}

7、相交链表

160. 相交链表 - 力扣(LeetCode)

思路:可能分叉的地方,前边或后边。但是对于链表不可能后边分叉,因为如果后边分叉了,当你遍历一条单链时,遍历到后边分叉的地方,就不知道继续走哪条路了,这样就不是单链表了。因此只有可能如上图的Y型,或者在一条直线上。

        如果同时各自出发,每次走一步,直到有一方到达尾结点,那么短的链表总是先到达,并且它们的最终距离是两链表的长度差。长度差来自分叉的部分,而不是相交的部分。那么如果让长的先走长度差步,再同时走,它们碰面的地方将是相交处。

public class Solution {

    private int size(ListNode head) {
        ListNode cur = head;
        int cnt = 0;
        while(cur != null) {
            cnt++;
            cur = cur.next;
        }
        return cnt;
    }

    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        // 求两条链的长度
        int size1 = size(headA);
        int size2 = size(headB);
        // 让长的作为 headA,求两链表长度差(正),
        int len = size1 - size2;
        if(size1 < size2) {
            len = size2 - size1;
            ListNode tmp = headA;
            headA = headB;
            headB = tmp;
        }
        // 让长的先走 len 步
        while(len != 0) {
            headA = headA.next;
            len--;
        }
        // 再同时走,相等的地方就是相交结点
        while(headA != headB) {
            headA = headA.next;
            headB = headB.next;
        }
        return headA;
    }
}

8、环形链表

141. 环形链表 - 力扣(LeetCode)

思路:慢指针和快指针同时走,如果有环,那么快、慢指针总会在环里相遇(快指针多跑 k 圈,然后追上慢指针);如果没环,快指针先走完全程结束。我们设慢指针每次走1步,快指针每次走2步。

        为什么快指针不能是 3、4、……、n 步?如果是3步,存在以下情况,无论走多久都不会相遇:

        快指针如果走 4 步,存在以下情况,无论走多久都不会相遇:

        以此类推……

public class Solution {
    public boolean hasCycle(ListNode head) {
        // 设置快、慢指针
        ListNode slow = head;
        ListNode fast = head;
        // slow 每次走1步,fast 每次走2步
        // 直到 fast = null(偶数个) 或者 fast.next = null(奇数个) 返回 flase
        // 或者 slow = fast 返回 true
        while(fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            if(slow == fast) {
                return true;
            }
        }
        return false;
    }
}

9、环形链表Ⅱ

142. 环形链表 II - 力扣(LeetCode)

思路

        若一指针 head1 从相遇点开始,一指针 head2 从头指针开始,同时走,每次走一步。当 head1 走 X 步到达入口;同时,head2 从相遇点开始走了 (k-1) 圈回到相遇点,再走 N 步到入口。即两指针相遇处,就是入口结点。

public class Solution {

    private ListNode getMeetNode(ListNode head) {
        ListNode slow = head;
        ListNode fast = head;
        while(fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            if(slow == fast) {
                return slow;
            }
        }
        return null;
    }

    public ListNode detectCycle(ListNode head) {
        // 获得 slow 与 fast 的相遇结点
        ListNode meetNode = getMeetNode(head);
        // 链表无环,返回 null
        if(meetNode == null) {
            return null;
        }
        // 分别从头指针,相遇结点开始走,两指针相遇处就是入口
        while(head != meetNode) {
            head = head.next;
            meetNode = meetNode.next;
        }
        return head;
    }
}

四、双向、不带头、非循环链表的实现

1、Node 内部类和属性

public class MyLinkedList  implements IList {
    private static class Node {
        int val;
        Node next; // 后继指针
        Node prev; // 前驱指针

        public Node(int val) {
            this.val = val;
        }
    }
    private Node head; // 头指针
    private Node tail; // 尾指针
    private int size; // 链表大小
    
    ............
}

2、清空链表

        与单向不同直接 head = null,双向需要遍历结点并释放 node,原因如下:head 为空,还有 node2 指向 node1,所以手动置 node2 的 pre 为空,node1释放;还有 node3 指向 node2,手动置 node3 的 pre 未空……

    @Override
    public void clear() {
        Node cur = head;
        while (cur!= null) {
            Node curN = cur.next;
            cur.prev = null;
            cur.next = null;
            cur = curN;
        }
        head = null;
        tail = null;
        size = 0;
    }

3、头插法

    @Override
    public void addFirst(int data) {
        Node newNode = new Node(data);
        if (head == null) {
            head = newNode;
            tail = newNode;
        } else {
            newNode.next = head;
            head.prev = newNode;
            head = newNode;
        }
        size++;
    }

4、尾插法

    @Override
    public void addLast(int data) {
        Node newNode = new Node(data);
        if (head == null) {
            head = newNode;
            tail = newNode;
        } else {
            tail.next = newNode;
            newNode.prev = tail;
            tail = newNode;
        }
        size++;
    }

5、任意位置插入

    @Override
    public void addIndex(int index, int data) {
        // 先检查索引是否越界
        checkIndexOutOfBounds(index);
        if (index == 0) { // 在 0 处插入,没有前驱节点
            addFirst(data);
            return;
        }
        if (index == size) { // 在末尾插入
            addLast(data);
            return;
        }
        Node newNode = new Node(data);
        // 找到插入位置
        Node cur = findIndex(index);
        // 插入新节点
        newNode.next = cur;
        newNode.prev = cur.prev;
        cur.prev.next = newNode;
        cur.prev = newNode;
        size++;
    }

6、删除第一次出现的 key

    private Node findKey(int key) {
        Node cur = head;
        while (cur != null) {
            if (cur.val == key) {
                return cur;
            }
            cur = cur.next;
        }
        return null;
    }

    @Override
    public void remove(int key) {
        Node deleteNode = findKey(key); // 找到待删除节点,如果不存在,返回 null
        if (deleteNode == null) { // 包含空链表的情况
            System.out.println("要删除的节点不存在。");
            return;
        }
        if (deleteNode == head) { // 待删除节点是头节点,包含了链表只有一个结点的情况
            head = deleteNode.next;
            if (head == null) { // 链表只有一个节点
                tail = null;
            } 
            else {
                head.prev = null;
            }
        } else if (deleteNode == tail) { // 待删除节点是尾节点
            tail = deleteNode.prev;
            tail.next = null;
        } else { // 待删除节点是中间节点
            deleteNode.prev.next = deleteNode.next;
            deleteNode.next.prev = deleteNode.prev;
        }
        size--;
    }

7、删除所有 key

    @Override
    public void removeAllKey(int key) {
        Node cur = head;
        while (cur != null) {
            if (cur.val == key) {
                if (cur == head) { // 待删除节点是头节点
                    head = cur.next;
                    if (head == null) { // 链表中只有一个节点
                        tail = null;
                    }
                    else {
                        head.prev = null;
                    }
                } else if (cur == tail) { // 待删除节点是尾节点
                    tail = cur.prev;
                    tail.next = null;
                } else { // 待删除节点是中间节点
                    cur.prev.next = cur.next;
                    cur.next.prev = cur.prev;
                }
                size--;
//                cur = cur.next; // 跳过已删除节点,继续遍历
            } /*else {
                cur = cur.next;
            }*/
            cur = cur.next;
        }
    }

五、LinkedList 的使用

        集合类中,LinkedList 的底层是双向链表

1、常用方法的使用

2、迭代器

        Iterator<E> 是集合类通用的迭代器,线性表专用的迭代器 ListIterator<E> 功能更强,可以反向迭代:

;