数据结构 - 链表

准备重启尘封一年的博客作为学习笔记,看看自己能坚持多久。
最近会记录做过的算法题,语言描述只用于会意,仅供参考。

0.从尾到头获取链表的值(不是反转链表)

解答思路

  • 最直观的想法是,遍历链表求出长度,创建空数组,再遍历链表将每个节点的值反向填进数组
  • 递归解法很简洁,思路类似于树的后根遍历(先根->递归过程自顶向下,后根->递归过程自底向上),从想象一棵单叉的树(其实就是链表),叶节点向根节点前进,就能得到反向的顺序
# -*- coding:utf-8 -*-
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = None

class Solution:
    # 返回从尾部到头部的列表值序列,例如[1,2,3]
    def printListFromTailToHead(self, listNode):
        if listNode is None:
            return []
        return self.printListFromTailToHead(listNode.next) + [listNode.val]

1.寻找/删除单链表倒数第k个节点

  • 使用两个指针(forward backward),起始时都指向头节点,首先让forward指针向前k步,到达第k+1个节点,然后让两指针以相同速度前进,直到forward为None,此时的backward指向的就是倒数第k个节点。
  • 对于删除问题,需要额外引入一个指针,指向backward之前的节点。此时会出现特殊情况:如果倒数第k个节点是链表的头节点,则需要特殊处理。
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        // 特判
        if (head.next == null && n == 1) {
            return null;
        }

        ListNode a = head;
        ListNode b = head;
        ListNode pre_b = null;

        // 先走a
        for (int i=0; i<n; i++) {
            a = a.next;
        }

        // ab同时走,直到a到达末尾
        while (a != null) {
            pre_b = b;
            a = a.next;
            b = b.next;
        }

        // 得到b是待删除结点
        // 如果b是首个元素,返回b.next
        if (pre_b == null) {
            return b.next;
        }
        // b非首个元素
        pre_b.next = b.next;
        return head;
    }
}

3.寻找单链表的中点

  • 使用快慢指针法。每次步进fast走两步,slow走一步,当fast或fast.next指向None时,slow即为中点。如果链表长度为偶数,则slow指向中间并靠后的节点
  • 注意循环判断条件 fast != null && fast.next != null
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    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;
    }
}

4.判断链表是否成环 / 求成环链表的起点

  • 快慢指针。如果fast最终能遇见None,则不成环。如果slow最终能遇见fast,则成环。
/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public boolean hasCycle(ListNode head) {
        ListNode fast = head;
        ListNode slow = head;

        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            if (fast == slow) {
                return true;
            }
        }

        return false;
    }
}

要求成环链表的起点,需要在fast和slow相遇时,将一个指针指向链表头,再让两指针以1步长前进直到相遇,相遇点即为环起点。
假设首次相遇时,fast比slow多走了k步,则这k步一定是在环内绕圈,即k是环长度的整数倍。此时让一个指针回到头节点,共同前进k-m步后一定会在环起点相遇。
在这里插入图片描述

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public ListNode detectCycle(ListNode head) {
        ListNode fast = head;
        ListNode slow = head;

        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next;
            if (fast != null) {
                fast = fast.next;
            }

            // 有环
            if (fast == slow) {
                fast = head;
            } else {
                continue;
            }
            while (fast != slow) {
                fast = fast.next;
                slow = slow.next;
            }

            return fast;
        }

        // 无环
        return null;
    }
}

5.判断两个链表是否相交

  • 关键在于解决两个链表的对齐问题。解决办法是,将链表A的末尾指向链表B的头节点,同理B的末尾指向A的头节点。使用两个指针a、b分别从A和B的头节点开始步进,直到遇见相同节点。
  • 直观思路:
    对于a,走过A的独有部分->相交部分->B的独有部分->相交部分
    对于b,走过B的独有部分->相交部分->A的独有部分->相交部分
  • 如果两个链表不相交,则 pa 和 pb 在对方末尾的 None 相等,返回 None
class Solution(object):
    def getIntersectionNode(self, headA, headB):
        """
        :type head1, head1: ListNode
        :rtype: ListNode
        """
        pa = headA
        pb = headB
        while (pa != pb):
            pa = pa.next if pa is not None else headB
            pb = pb.next if pb is not None else headA
        return pa

6.反转整个链表

迭代法

对于每个节点分为三个步骤:

temp = node.next  // 记录当前节点的下个节点,防止修改后找不到
node.next = pre  // 反转
pre = node  // 步进
node = temp  // 步进
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode reverseList(ListNode head) {
        // 迭代
        ListNode temp = head;  // 从head开始处理,head.next最后指向null
        ListNode pre_temp = null;
        while (temp != null) {
            ListNode next_node = temp.next;  // 保存
            temp.next = pre_temp;  // 更改指针
            pre_temp = temp;  // 步进
            temp = next_node;
        }
        return pre_temp;
    }
}

递归法**

  • 来自拉不拉东的算法小抄:写递归算法的关键是,明确函数的定义是什么,“相信定义”,用其推导出最终结果,而不陷入递归的细节。搞清楚根节点该做什么,剩下的交给前中后序遍历
  • 写递归时要有遍历树的思想。下面的代码即为树的后根遍历
  • 反转等操作(类似题1)都能用后根的思想解决
# Definition for singly-linked list.
# class ListNode(object):
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution(object):
    def reverseList(self, head):
        """
        :type head: ListNode
        :rtype: ListNode
        """
        # 递归出口
        if head is None or head.next is None:
            return head

        reversed = self.reverseList(head.next)
        head.next.next = head
        head.next = None

        return reversed

7.反转链表的从第left到第right个节点

迭代法

  • 首先定位到第left个节点,记录边界,并执行right-left次反转
# Definition for singly-linked list.
# class ListNode(object):
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution(object):
    def reverseBetween(self, head, left, right):
        """
        :type head: ListNode
        :type left: int
        :type right: int
        :rtype: ListNode
        """
        dummy = ListNode(val=-1, next=head)
        pre_node = dummy
        temp_node = head

        # 移动到第left个节点
        for i in range(left-1):
            pre_node = temp_node
            temp_node = temp_node.next
        
        # 记录左侧未被反转的最后一个节点,和被反转的第一个节点
        forward_last_node = pre_node
        reverse_first_node = temp_node

        # 从开始反转的第二个节点开始,改变连接关系
        pre_node = pre_node.next
        temp_node = temp_node.next

        # 执行反转
        for i in range(right-left):
            temp = temp_node.next
            temp_node.next = pre_node
            pre_node = temp_node
            temp_node = temp

        # 连接
        reverse_first_node.next = temp_node
        forward_last_node.next = pre_node

        return dummy.next

递归法

首先写一个反转头部前N个节点的函数reverseN

ListNode reverseBetween(ListNode head, int m, int n) {
    // 递归出口
    if (m == 1) {
        return reverseN(head, n);
    }
    
    head.next = reverseBetween(head.next, m - 1, n - 1);
    return head;
}

8.K个一组反转列表

class Solution:
    def reverseKGroup(self , head , k ):
        # write code here
        if not head or k == 1:
            return head
        dummy = ListNode(-1)
        node = head 
        
        last_left = None 
        while (self.enough(node, k)):  # 每次反转k个
            pre = node 
            cur = node.next
            # 执行反转
            for _ in range(k - 1):
                temp = cur.next
                cur.next = pre 
                pre = cur 
                cur = temp 
            if node == head:  # 记录返回结果
                dummy.next = pre 
            # 处理边界
            if last_left:
                last_left.next = pre 
            last_left = node 
            node = cur 
            
        if last_left:  # 连接最后不到k个不反转的
            last_left.next = node 
            
        return dummy.next if last_left else head 
    
    # 判断从当前节点开始是否够k个
    def enough(self, node, k):
        cnt = 0
        while node:
            node = node.next
            cnt += 1
            if cnt >= k:
                return True 
        return False 

就这么做!

class Solution {
    public ListNode reverseKGroup(ListNode head, int k) {
        ListNode prevLeftNode = null;  // 记录上一组反转后的最后节点
        ListNode res = null;  // 记录结果
        ListNode prev = head;
        ListNode curr = head.next;
        
        
        while (this.hasKNodesLeft(prev, k)) {
            ListNode groupHead = prev;  // 记录当前组反转前的首个节点
            
            // 组内反转k-1次
            for (int i = 0; i < k - 1; i++) {
                ListNode temp = curr.next;
                curr.next = prev;
                prev = curr;
                curr = temp;
            }
            
            // 更改和前一组的指针
            if (prevLeftNode != null) {
                prevLeftNode.next = prev;  // 非第一组,改变指向
            } else {
                res = prev;  // 第一组,记录结果
            }

			// 更改循环条件
            prevLeftNode = groupHead;
            prevLeftNode.next = curr;  // 先把上一组的最后值指向下一组(可能不够k个)的首个位置,如果够k个再修改
            prev = curr;
            curr = curr == null ? null : curr.next;  // 避免下一组无元素
        }

        return res;
    }

    /* 判断是否足够k个节点 */
    public boolean hasKNodesLeft(ListNode node, int k) {
        int count = 0;
        while (node != null && count < k) {
            count += 1;
            node = node.next;
        }

        return count == k ? true : false;
    }
}

9.判断回文链表

方法一,使用递归,使用树的后根遍历思想,实现栈“后进先出”的功能。

// 左侧指针
ListNode left;

boolean isPalindrome(ListNode head) {
    left = head;
    return traverse(head);
}

boolean traverse(ListNode right) {
    if (right == null) return true;
    boolean res = traverse(right.next);
    // 后序遍历
    res = res && (right.val == left.val);
    left = left.next;
    return res;
}

方法二,利用快慢指针寻找中点,反转中点以后的链表并进行比较,推荐

class Solution(object):
    def isPalindrome(self , head ):
        if not head:
            return True 
        
        slow, fast = head, head 
        while (fast):
            slow = slow.next 
            fast = fast.next 
            if fast:
                fast = fast.next 
        # 反转后半部分
        post = self.reverse(slow) 
        # 逐个比较
        pre = head 
        while (pre and post):
            if pre.val != post.val:
                return False 
            pre = pre.next 
            post = post.next 
        return True 
        
    def reverse(self, node):
        if not node:
            return None
        elif not node.next:
            return node 
        
        r = self.reverse(node.next)
        node.next.next = node 
        node.next = None 
        return r

10.复制复杂的链表

链表的每个节点都具有val, next, random属性。关键点是random如何获取——下标如何获取——借用list / 哈希表

"""
class Solution:
    # 返回 RandomListNode
    def Clone(self, pHead):
        if not pHead:
            return pHead 
        
        dummy = RandomListNode(-1)
        head = RandomListNode(pHead.label)
        dummy.next = head 
        
        # 先复制原链表,用两个数组分别记录新老链表的节点
        olist = [pHead]
        nlist = [head]
        p = pHead.next 
        while (p):
            head.next = RandomListNode(p.label)
            head = head.next 
            nlist.append(head)
            olist.append(p)
            p = p.next 
        
        # 连接random
        for i in range(len(olist)):
            target = olist[i].random 
            if not target:
                continue 
            idx = olist.index(target)
            nlist[i].random = nlist[idx]
            
        return dummy.next 

更优的解法

class Solution {
    public Node copyRandomList(Node head) {
        if (head == null) {
            return null;
        }

        
        List<Node> nodes = new ArrayList<>();  // 存放复制的节点
        Map<Node, Integer> mapping = new HashMap<>();  // 用哈希表记录节点到下标的映射

		// 复制值,并记录下标
        Node node = head;
        int index = 0;
        while (node != null) {
            nodes.add(new Node(node.val));
            mapping.put(node, index);
            index++;
            node = node.next;
        }

        // 执行连接
        node = head;
        for (int i = 0; i < nodes.size(); i++) {
            // 1.连接random
            Node temp = nodes.get(i);
            if (node.random != null) {
                int randomIndex = mapping.get(node.random);
                temp.random = nodes.get(randomIndex);
            }
            // 2.连接next
            if (i < nodes.size() - 1) {
                temp.next = nodes.get(i + 1);
            }
            node = node.next;
        }

        return nodes.get(0);
    }
}

11.链表加法

在这里插入图片描述

  • 借助栈实现后端对齐
  • 处理最后一步的进位问题
class Solution:
    def addInList(self , head1 , head2 ):
        # write code here
        # 用栈
        s1, s2 = [], []
        while head1:
            s1.append(head1)
            head1 = head1.next 
        while head2:
            s2.append(head2)
            head2 = head2.next 
        
        post = None 
        plus = 0
        while (s1 and s2):
            n1, n2 = s1.pop(), s2.pop()
            val = n1.val + n2.val + plus 
            plus = 1 if val >= 10 else 0
            val -= 10 if val >= 10 else 0
            node = ListNode(val)
            node.next = post 
            post = node 
        if s1:
            while s1:
                n1 = s1.pop()
                val = n1.val + plus 
                plus = 1 if val >= 10 else 0
                val -= 10 if val >= 10 else 0
                node = ListNode(val)
                node.next = post 
                post = node 
        elif s2:
            while s2:
                n2 = s2.pop()
                val = n2.val + plus 
                plus = 1 if val >= 10 else 0
                val -= 10 if val >= 10 else 0
                node = ListNode(val)
                node.next = post 
                post = node
        if plus == 1:
            res = ListNode(1)
            res.next = post 
            return res 
        return post 

12.删除排序链表的重复节点

添加链接描述
在这里插入图片描述

  • 递归解法
class Solution {

    int lastDuplicate = -999;  // 记录上次发生重复的值

    public ListNode deleteDuplicates(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }
        // 删除后续节点的重复值
        ListNode deleted = deleteDuplicates(head.next);
        // 判断当前节点是否和后续重复
        if (head.val != lastDuplicate && (deleted == null || deleted.val != head.val)) {  // 1.不重复
            head.next = deleted;
            return head;
        } else {  // 2.发生重复
            lastDuplicate = head.val;
            if (deleted == null || deleted.val != lastDuplicate) {  // delete节点的值不参与重复,不删除delete节点
                return deleted;
            } else {
                return deleted.next;  // 删除detele节点
            }
        }
    }
}

13. 排序链表

添加链接描述

  • 快慢指针寻找链表中点,左右递归执行排序
  • 合并排序结果
    在这里插入图片描述
class Solution {
    public ListNode sortList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }
        ListNode fast = head;
        ListNode slow = head;
        ListNode slowPrev = slow;
        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            slowPrev = slow;
            slow = slow.next;
        }
        // 划分并排序
        slowPrev.next = null;
        ListNode l = sortList(head);
        ListNode r = sortList(slow);
        // 合并
        ListNode dummy = new ListNode(-1);
        ListNode curr = dummy;
        while (l != null && r != null) {
            if (l.val <= r.val) {
                curr.next = l;
                curr = l;
                l = l.next;
            } else {
                curr.next = r;
                curr = r;
                r = r.next;
            }
        }
        curr.next = (l == null) ? r : l;
        return dummy.next;
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值