zoukankan      html  css  js  c++  java
  • 算法小技巧 -- 链表

    一、快慢指针

    1、核心思想

    【核心思想:】
        采用双指针完成,一个指针永远比另一个指针稍快一点。
    
    【常见案例:】
        找到单链表的中间节点
        环形链表 【单链表结构:】
    class ListNode { int val; ListNode next; ListNode() {} ListNode(int val) { this.val = val; } ListNode(int val, ListNode next) { this.val = val; this.next = next; } }

    2、案例实现(找到单链表的中间节点)

    【LeetCode 题目:】
    标题:
        876. 链表的中间结点
        
    题目描述:
        给定一个头结点为 head 的非空单链表,返回链表的中间结点。
        如果有两个中间结点,则返回第二个中间结点。
    
    限制:
        给定链表的结点数介于 1 和 100 之间。
    
    测试用例:
        输入:[1,2,3,4,5]
        输出:3 
    
        输入:[1,2,3,4,5,6]
        输出:4
    
    【案例分析:】
        假设两个指针 slow、fast。
        每次指向下一个节点时,fast 都比 slow 快一个节点,
        即 fast 每次跳过两个节点,slow 每次跳过一个节点。
        当 fast 遍历到链表末尾时,slow 恰好处于链表中间节点处。
    即
        slow = slow.next;
        fast = fast.next.next;
    注:
        链表节点为偶数时,存在两个中间节点,此处以第二个节点作为中间节点。
        
    【伪代码实现:】
    public ListNode middleNode(ListNode head) {
        ListNode slow = head;
        ListNode fast = head;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        return slow;
    }

    3、案例实现(环形链表)

    (1)LeetCode 题目

    【LeetCode 题目:】
    标题:
        141. 环形链表
        
    题目描述:
        给定一个链表,判断链表中是否有环。
        如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 
        如果链表中存在环,则返回 true。 否则,返回 false 。
    注:
        为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。
        如果 pos 是 -1,则在该链表中没有环。
        pos 不作为参数进行传递,仅仅是为了标识链表的实际情况(用于表示哪个位置开始会出现环)。
    
    限制:
        链表中节点的数目范围是 [0, 104]
        -105 <= Node.val <= 105
        pos 为 -1 或者链表中的一个 有效索引 。
    
    测试用例:
        输入:head = [3,2,0,-4], pos = 1
        输出:true
        解释:链表中有一个环,其尾部连接到第二个节点。
        
        输入:head = [1,2], pos = 0
        输出:true
        解释:链表中有一个环,其尾部连接到第一个节点。
        
        输入:head = [1], pos = -1
        输出:false
        解释:链表中没有环。

    (2)案例分析

    【案例分析:】
        链表存在环,则进行链表遍历时将会进入 死循环,需要设置结束遍历的条件。
        若不存在环,即使链表足够长,总能遍历结束,无需设置结束遍历的条件。
    联想到一个场景:
        晚上寂寞无聊,约哥们去体育馆操场跑圈。假设两个人为 A、B。
        A、B 同时出发,由于身体素质问题,A 跑的比 B 快点。
        经过一段时间后,A 甩了 B 半圈,再经过一段时间后,A 追上了 B。
        只要操场跑道是个圈,A 总能追上 B,只是时间长短的问题。
    即:
        A、B 的跑步频率只要设置的不太离谱,总有重合的一天。
        此处设置 A 为快指针,B 为慢指针。
        A 跑两米,B 跑一米,当 A 追上了 B,则表示是个环。    

    (3)伪代码

    【伪代码:】
    public boolean hasCycle(ListNode head) {
        if (head == null) {
            return false;
        }
        ListNode slow = head;
        ListNode fast = head;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            if (slow == fast) {
                return true;
            }
        }
        return false;
    }

    二、单链表的遍历

    1、核心思想

    【核心思想:】
        head 与 head.next 一定要区分开。
        每次经过一个节点后,需要跳到下一个节点,
    即 
        head = head.next。
    
    【常见实现:】
        使用 while 进行遍历。
        使用 递归 进行遍历。(类似于树的 前序、后序 遍历的实现)
        
    【常见案例:】
        正序输出链表(可使用 while、递归 实现)
        反序输出链表(可使用 递归 实现)

    2、案例实现(正序输出链表 -- while 实现)

    【案例分析:】
        temp 与 temp.next 一定要区分开。
        temp 表示当前所在链表节点位置,temp.next 表示当前链表节点的下一个位置。
    注:
        判断条件可以为 temp != null 或者 temp.next != null,灵活使用。
    
    【伪代码:】
    public void printList(ListNode head) {
        ListNode temp = head;
        if (temp == null) {
            System.out.println("Empty");
            return;
        }
        while(temp != null) {
            System.out.println(temp.val);
            temp = temp.next;
        }
    }

    3、案例实现(遍历输出链表 -- 递归实现)

    【案例分析:】
        遍历输出链表,正序、反序 的代码非常相似,可等同于 树结构的 前序、后序遍历进行理解。
        递归 类似于 栈结构,先进后出。入栈的顺序就是 正序的,出栈的顺序就是 反序的。
    
    【伪代码:】
    public static void printList(ListNode head) {
        if (head == null) {
            System.out.println("Empty");
            return;
        }
        nextNode(head);
    }
    
    public static void nextNode(ListNode head) {
        if (head == null) {
            return;
        }
        // System.out.println(head.val); // 正序遍历输出
        nextNode(head.next);
        System.out.println(head.val); // 反序遍历输出
    }

    三、单链表反转

    1、核心思想

    【核心思想:】
        head 与 head.next 一定要区分开。
        每经过一个节点,确保当前节点的下一个节点指向自己。
    即:
        head.next.next = head
        若从头开始反转,可以使用 多指针 实现。
        若从尾开始反转,可以使用 递归 实现。
    
    【常见实现:】
        使用 栈 作为中转站,遍历链表,将值入栈,出栈时构建为一个新的链表。
        使用多个指针,从头开始,逐个节点进行反转。
        使用 递归 进行反转,从尾开始,逐个节点进行反转。
        
    【常见案例:】
        反转链表
        反转部分链表(指定范围进行反转)
        K 个一组翻转链表
        回文链表

    2、案例实现(反转链表 -- 栈实现)

    【LeetCode 题目:】
    标题:
        206. 反转链表
    
    题目描述:
        给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
    
    限制:
        链表中节点的数目范围是 [0, 5000]
        -5000 <= Node.val <= 5000
    
    测试用例:
        输入:head = [1,2,3,4,5]
        输出:[5,4,3,2,1]
        
        输入:head = [1,2]
        输出:[2,1]
        
        输入:head = []
        输出:[]
    
    【案例分析:】
        使用 栈 作为中转站,遍历链表,将值入栈,出栈时构建为一个新的链表。
    
    【伪代码:】
    public ListNode reverseList(ListNode head) {
        if (head == null) {
            return head;
        }
        ListNode temp = head;
        Stack<Integer> stack = new Stack<>();
        while(temp != null) {
            stack.push(temp.val);
            temp = temp.next;
        }
        temp = head;
        while(!stack.isEmpty()) {
            temp.next = new ListNode(stack.pop());
            temp = temp.next;
        }
        return head.next;
    }

    3、案例实现(反转链表 -- 多指针实现)

    【LeetCode 题目:】
    标题:
        206. 反转链表
    
    【案例分析:】
        此方式从头向尾执行。
        使用多个指针,假设当前指针为 current,其上一个指针为 pre,下一个指针为 next。
    其中:
        pre 用于存储 当前指针 需要指向的 上一个节点。
        next 用于存储 当前指针 需要跳到的 下一个节点。
        每次经过一个节点时,保证当前节点指向上一个节点,然后跳转到下一个节点继续操作。
    即:
        next = current.next;
        current.next = pre;
        pre = current;
        current = next;
        
    【伪代码:】
    public ListNode reverseList(ListNode head) {
        ListNode pre = null;
        ListNode next = null;
        ListNode current = head;
        while(current != null) {
            next = current.next;
            current.next = pre;
            pre = current;
            current = next;
        }
        return pre;
    }

    4、案例实现(反转链表 -- 递归实现)

    【LeetCode 题目:】
    标题:
        206. 反转链表
    
    【案例分析:】
        此方式从尾向头进行。
        每次经过一个节点时,保证当前节点的下一个节点指向当前节点,移除当前节点指向的下一个节点。
        然后跳转到上一个节点继续操作(此过程由递归的逻辑实现,返回上一层)。
    即
        head.next.next = head;
        head.next = null;
    
    【伪代码:】
    public static ListNode reverseList(ListNode head) {
        if (head == null) {
            return head;
        }
        return nextNode(head);
    }
    
    public static ListNode nextNode(ListNode head) {
        if (head.next == null) {
            return head;
        }
        // lastNode 表示最后一个节点位置,没有被修改,不断的向递归的上层传递
        ListNode lastNode = nextNode(head.next);
        head.next.next = head;
        head.next = null;
        return lastNode;
    }

    5、案例实现(反转部分链表 -- 多指针)

    【LeetCode 题目:】
    标题:
        92. 反转链表 II
    
    题目描述:
        给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。
        请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。
    
    限制:
        链表中节点数目为 n
        1 <= n <= 500
        -500 <= Node.val <= 500
        1 <= left <= right <= n
    
    测试用例:
        输入:head = [1,2,3,4,5], left = 2, right = 4
        输出:[1,4,3,2,5]
        
        输入:head = [5], left = 1, right = 1
        输出:[5]
    
    【案例分析:】
        反转指定范围的链表。关键就在于 反转范围边界 的四个节点是如何连接的。
        假设反转范围边界节点分别为 a、b、c、d,需要反转的是 b -> ... -> c 这部分节点。
        开始顺序为 head -> ... a -> b -> ... -> c -> d -> ...
        最终需要得到的顺序为 head -> ... a -> c -> ... -> b -> d -> ...
        这么看上去就很直观了,先令 b、c 之间的链表节点反转,然后让 a 指向 c,b 指向 d 即可完成。
    注:
        范围为 0 时,此时只反转一个节点,即 b、c 为同一个节点,无需反转。    
        若从头开始反转,即没有 a 节点,此时 c 直接作为新的头结点即可。
    
    【伪代码:】
    public ListNode reverseBetween(ListNode head, int left, int right) {
        ListNode first, temp;
        first = temp = head;
        int count = right - left;
        if (count != 0) {
            if (left == 1) {
                // 从头开始反转,没有 a 节点,c 节点即为新的头结点
                return reverseList(head, count);
            }
            while (temp != null && left != 1) {
                left--;
                // 找到开始反转的节点的上一个节点,即 a 节点
                first = temp;
                // 找到开始反转的节点,即 b 节点
                temp = temp.next;
            }
            // 从中间位置开始反转,需要连接上反转范围的链表的新头节点,即 a -> c
            first.next = reverseList(temp, count);
        }
        return head;
    }
    
    public ListNode reverseList(ListNode head, int count) {
        ListNode first, pre, next, current;
        pre = next = null;
        first = current = head;
        // 反转指定范围的链表
        while(current != null && count != 0) {
            next = current.next;
            current.next = pre;
            pre = current;
            current = next;
            count--;
        }
        // 指向反转范围的新尾节点的下一个节点,即 b -> d
        first.next = current.next;
        // 返回反转范围链表的新头结点,即 c 节点
        current.next = pre;
        return current;
    }

    6、案例实现(K 个一组翻转链表)

    【LeetCode 题目:】
    标题:
        25. K 个一组翻转链表
    
    题目描述:
        给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。
        k 是一个正整数,它的值小于或等于链表的长度。
        如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
        你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
    
    限制:
        列表中节点的数量在范围 sz 内
        1 <= sz <= 5000
        0 <= Node.val <= 1000
        1 <= k <= sz
    
    测试用例:
        输入:head = [1,2,3,4,5], k = 2
        输出:[2,1,4,3,5]
        
        输入:head = [1,2,3,4,5], k = 3
        输出:[3,2,1,4,5]
        
        输入:head = [1,2,3,4,5], k = 1
        输出:[1,2,3,4,5]
        
        输入:head = [1], k = 1
        输出:[1]
        
    【案例分析:】
        k 个节点一组去反转,原理与 部分节点反转 类似,只是多了重复调用的过程。
        关注点同样是 a、b、c、d 四个节点指向的问题。
    注:
        此处从头开始反转,即第一次没有 a 节点,直接使用 c 节点作为新的头节点。
    
    【伪代码:】
    public ListNode reverseKGroup(ListNode head, int k) {
        if (head == null) {
            return head;
        }
        ListNode temp = head;
        // 找到需要反转的节点范围
        for (int i = 0; i < k; i++) {
            // 若反转节点范围不足,则不进行反转
            if (temp == null) {
                return head;
            }
            temp = temp.next;
        }
        // 获取反转节点后的新头结点
        ListNode newHead = reverseList(head, k - 1);
        // 进行下一次反转,用于获取下一部分的新头节点,并令当前尾节点指向下一次获取的头结点
        head.next = reverseKGroup(temp, k);
        return newHead;
    }
    
    public ListNode reverseList(ListNode head, int count) {
        //ListNode first, pre, next, current;
        ListNode pre, next, current;
        pre = next = null;
        //first = current = head;
        current = head;
        while (current != null && count != 0) {
            next = current.next;
            current.next = pre;
            pre = current;
            current = next;
            count--;
        }
        //first.next = current.next;
        current.next = pre;
        return current;
    }

    7、案例实现(回文链表)

    【LeetCode 题目:】
    标题:
        剑指 Offer II 027. 回文链表
        
    题目描述:
        给定一个链表的 头节点 head ,请判断其是否为回文链表。
        如果一个链表是回文,那么链表节点序列从前往后看和从后往前看是相同的。
        能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?
        
    限制:
        链表 L 的长度范围为 [1, 105]
        0 <= node.val <= 9
    
    测试用例:
        输入: head = [1,2,3,3,2,1]
        输出: true
        
        输入: head = [1,2]
        输出: fasle
        
    【案例分析:】
        判断回文,常用方法就是:从两侧向中间逼近 或者 从中间向两侧展开 或者 暴力反转构建新的链表,然后逐个比较。
    从两侧向中间逼近(效率低):
        由于链表的结构特殊,无法直接从 尾节点向头结点遍历,
        但可以使用 递归的方式,模拟 栈 的使用,先递归到最后一个节点,然后逐级回退到上层节点。
    从中间向两侧逼近(效率稍高):
        先找到链表的中点,可以使用快慢指针。
        然后对 前半段链表进行 反转,这样就可以从 中间节点 向两侧展开并比较。
        当然,也可以反转后半段链表,然后从头结点开始比较。
    暴力反转构建新链表(需要额外空间):
        直接对链表进行反转,构建一个新链表。
        然后 新链表、旧链表 逐个进行值比较。
    
    【伪代码:(从两侧向中间逼近)】
    public static ListNode first = null;
    public static boolean isPalindrome(ListNode head) {
        first = head;
        return nextNode(head);
    }
    
    public static boolean nextNode(ListNode head) {
        if (head == null) {
            return true;
        }
        boolean flag = nextNode(head.next);
        if (flag && head.val == first.val) {
            first = first.next;
            return flag;
        }
        return false;
    } 
    
    【伪代码:(从中间向两侧展开,反转后半段链表)】
    public static boolean isPalindrome(ListNode head) {
        // 找中间节点
        ListNode middleNode = middleNode(head);
        // 反转后半段链表
        ListNode newNode = reverseList(middleNode);
        // 从头结点 开始比较 新的后半段链表
        while(newNode != null && head != null) {
            if (newNode.val != head.val) {
                return false;
            }
            newNode = newNode.next;
            head = head.next;
        }
        return true;
    }
    
    public static ListNode middleNode(ListNode head) {
        ListNode slow = head;
        ListNode fast = head;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        return slow;
    }
    
    public static ListNode reverseList(ListNode head) {
        ListNode pre, next, current;
        pre = next = null;
        current = head;
        while (current != null) {
            next = current.next;
            current.next = pre;
            pre = current;
            current = next;
        }
        return pre;
    }

    别把自己太当回事,也别太把自己不当回事!Life is Fantastic!!!
  • 相关阅读:
    UI控件
    iOS 上架
    UISwitch 开关
    UISlider 的属性
    SQL SERVER 触发器
    sql server数据库操作
    二叉堆实现优先队列
    散列表
    AVL树
    C++基础-02
  • 原文地址:https://www.cnblogs.com/l-y-h/p/15250530.html
Copyright © 2011-2022 走看看