链表不得不刷的进阶题目,牛客TOP101链表相关——链表刷题

发布于:2023-01-14 ⋅ 阅读:(167) ⋅ 点赞:(0)

wall-g97cb7842a_1920

前言:

紧承上一篇 链表基础题目分享 的博客,本篇博客题目多来自于牛客 TOP101 中链表相关,难度较上一篇稍难半分。博主希望通过言简意赅的思路和代码,为大家提供不同的解题灵感,受限于精力原因只有少部分题目包含了详细图解,很抱歉,后续可能会进行补全。期盼能大家能在下面的链表题解中有所启发思考,共勉。



目录:




1 链表内指定区间反转


链表内指定区间反转

1)思路:

(1)区间头插:

  • 添加表头节点newHead,结果返回 newHead.next

  • 初始化pre,cur,pre cur再向前, m - 1 步到达反转区间

    • pre保存 m节点的上一个节点(m节点会随头插发生变化),cur节点保存第一次到达m位置的节点
  • 区间内元素进行头插

    • 保存 curNext
    • cur 移向curNext.next
    • 配合 pre 将 curNext 进行头插

(2) 递归尾插:

图示

区间反转1


  • reverseBetween 递归, 找到反转区间

    • 子问题思路, 直到 m == 1 找到反转区间
    • 中止条件: m == 1 开始进行reverse 反转
    • 本级目标: 不断缩短区间, 将本级节点 head 和子问题中反转后的子链表进行拼接
    • 返回值: 返回拼接后的新链表的头结点
  • reverse 递归, 进行 “尾插” 反转

    • 子问题思路, 直到 n == 1 找到 n 节点开始 “归” 进行尾插
    • 中止条件: n == 1 找到 n 节点, 同时保存 n 节点下一个节点
    • 本级目标: 缩短 n 值, 子问题结束后将本级节点 head 尾插到 n 节点之后
    • 返回值: 返回子问题反转后的头结点, 也就是未反转区间的 n 节点


2)解法:

  • 区间头插
public ListNode reverseBetween (ListNode head, int m, int n) {
        // write code here
        //最后返回 表头节点的下一个节点即可,因为 head 位置可能会发生改变 
        ListNode newHead = new ListNode(-1);
        newHead.next = head;
        
        //通过pre 节点可以找到上一次移向 m 位置的节点
        ListNode pre = newHead;
        ListNode cur = head;
        
        //到达 m 节点
        for(int i = 1; i < m; i++){
            pre = cur;
            cur = cur.next;
        }
        
        //将区间内元素进行头插
        //通过改变 cur.next 达到cur向前移动的目的,且保持 cur 内容不变
        for(int i = m; i < n; i++){
            //cur 向前移动
            ListNode curNext = cur.next;
            cur.next = curNext.next;
            //curNext 进行头插
            curNext.next = pre.next;
            pre.next = curNext;
        }
        
        return newHead.next;
    }

  • 递归尾插
public ListNode reverseBetween (ListNode head, int m, int n) {
        //m == 1到达反转区间,进行反转, 反转完成直接返回
        if(m == 1){
            return reverse(head, n);
        }
        
        //进入子问题,保存反转过后子链表的头结点
        ListNode node = reverseBetween(head.next, m - 1, n - 1);
        
        //将本级节点和反转后子链表进行拼接
        head.next = node;
        
        //返回拼接后的新链表的头结点
        return head;
        
    }
    
    //n节点的 next 节点
    public ListNode finalNext = null; 
    
    public ListNode reverse(ListNode head, int n){    
        //到达最后一个节点直接返回, 同时保存 n 节点下一个节点
        if(n == 1){
            finalNext = head.next;
            return head;
        }
        
        //进入反转的子问题, 保存的 node 是未经过反转的 n 节点, 该节点也是区间所有节点反转完成后的 区间头结点
        ListNode node = reverse(head.next, n - 1);
        
        //进行 "尾插" 反转, 因为本级节点 head 总是子区间反转过后 n 节点的上一个节点
        head.next.next = head;
        
        //head 指向 finalNext 节点, 成为新的 n 节点 
        head.next = finalNext;
        
        //返回 区间头结点
        return node;
        
    }



2 链表中的节点每k个一组翻转


链表中的节点每k个一组翻转

1 ) 思路:

(1)求组数反转:

  • 链表长度求反转组数 num
  • 定义 cur pre 进行反转 num组

(2)递归:

  • 中止条件: head == null 或者 反转次数不足 k 个
  • 本级任务: cur pre 进行反转, 反转结束 pre 到达表头, head变成表尾, head.next 指向子问题返回的表头节点
  • 返回值: 返回反转链表的头节点


2)解法:

  • 求组数反转
	public ListNode reverseKGroup (ListNode head, int k) {
        // write code here
        ListNode newHead = new ListNode(-1);
        newHead.next = head;
        //计算链表长度 算出反转次数
        int count = 0;
        ListNode cur = head;
        while(cur != null){
            cur = cur.next;
            count++;
        }
        
        int num = count / k;
        
        //遍历链表进行反转
        ListNode pre = newHead;
        cur =  head;
        for(int i = 0; i < num; i++){
            pre = reverse(cur, pre,  k);
            cur = pre.next;
        }
        
        return newHead.next;
    }
    
    public ListNode reverse(ListNode cur, ListNode pre, int k){
        ListNode curNext = null;
        for(int i = 1; i < k; i++){
            //cur 向前
            curNext = cur.next;
            cur.next = curNext.next;
            
            //头插
            curNext.next = pre.next;
            pre.next = curNext;
        }
        
        pre = cur;
        return pre;
    }
  • 递归
	public ListNode reverseKGroup (ListNode head, int k) {
        //链表反转的最后节点的下一个节点
        ListNode tail = head;
        
        //中止条件, head 为空 或者 反转不足 k 个
        for(int i = 0; i < k; i++){
            if(tail == null){
                return head;
            }
            tail = tail.next;
        }
        
        ListNode cur = head;
        ListNode pre = null;
        
        //进行反转
        while(cur != tail){
            ListNode curNext = cur.next;
            cur.next = pre;
            pre = cur;
            cur = curNext;
        }
        
        //头节点反转过后变为尾节点 指向下一个子问题求得反转链表的头结点
        head.next = reverseKGroup(tail, k);
        //pre 节点变为反转过后的新链表头结点, 作为子问题的结果返回
        return pre;
    }



3 合并两个排序的链表


合并两个排序的链表

1 ) 思路

(1)迭代

  • 使用 带虚拟头结点 的新链表保存排序节点
    • 带虚拟头结点链表保证每个节点都有前驱, 称为 “哨兵”
  • 找到当前 list1.val 和 list2.val 的较小值保存到新链表中 (取等默认保存 list1.val) , list1 或 list2 向后, 如此 循环直到 list1 || list2 == null
  • 将非空的 list1 / list2 拼接到新链表后面

(2)递归

  • 中止条件: list1 / list2 == null
  • 本级目标: 找到 当前 list1 和 list2 中最小的元素节点, 通过 子问题重构 当前最小节点的下一个节点
    • 重构进行递归缩小查找区间
  • 返回值: 返回最小节点,因为找到最小元素可能在 list1 也可能在 list2中,所以要 分开讨论返回不同值


2 ) 解法

  • 迭代
public ListNode Merge(ListNode list1,ListNode list2) {
        //迭代
        if(list1 == null) {
            return list2;
        }
        
        if(list2 == null){
            return list1;
        }
        
        ListNode newHead = new ListNode(0);
        ListNode cur = newHead;
        
        while(list1 != null && list2 != null){
            if(list1.val < list2.val){
                cur.next = list1;
                list1 = list1.next;
            }else{
                cur.next = list2;
                list2 = list2.next;
            }
            
            cur = cur.next;
        }
        
        if(list1 != null){
            cur.next = list1;
        }else{
            cur.next = list2;
        }
        
        return newHead.next;
    }

  • 递归
public ListNode Merge(ListNode list1,ListNode list2) {    
        //中止条件 list1 / list2 == null 
        if(list1 == null) return list2;
        if(list2 == null) return list1;
        
        //找到 当前 list1 和 list2 中最小的元素节点, 递归缩小区间,找到当前最小节点的下一个节点
    	//子问题完成后返回新链表的头节点
        if(list1.val <= list2.val){
            list1.next = Merge(list1.next, list2);
            return list1;
        }else{
            list2.next = Merge(list1, list2.next);
            return list2;
        }
    }



4 合并k个已排序的链表


合并k个已排序的链表

1 ) 思路:

(1)优先级队列

  • 创建堆小堆保存每个链表头结点
    • 使用比较器 new Comparator (){ @Override 重写compare} 创建小堆
    • lambda表达式 (e1, e2) -> e1.val - e2.val 创建小堆
    • 保存节点判空
  • 出队,元素 放入新链表, 出队元素后继进行判空入队

(2) 归并排序

  • 将 k 个元素看作 k 个子问题, 通过递归中 “递” 的过程 将区间缩小划分出子问题 (依靠下标进行划分), 到达 “归” 的过程将 子问题返回的链表两两一组进行合并, 成为一个新链表, 随后不断向上合并
    • 中止条件: 只有一个节点或者 没有节点了
    • 本级目标: 使用 mid 划分区间, 将 mid 分开的左右两区间进行子问题排序, 合并成两张表, 再将两新表进行合并
    • 返回值: 合并完成后新链表的头结点
  • tip:
    • 子问题返回新链表 加上合并返回 可以优化成一步
    • ArrayList 通过 get() 方法获取到下标元素
    • 合并两链表可以使用递归进行


2 ) 解法:

  • 优先级队列
public ListNode mergeKLists(ArrayList<ListNode> lists) {
        //1 创建堆小堆保存每个头结点
        PriorityQueue<ListNode> minHeap = new PriorityQueue<>(new Comparator<ListNode>(){
            @Override
            public int compare(ListNode e1, ListNode e2){
                return e1.val - e2.val;
            }
        });
        
        for(ListNode node : lists){
            if(node != null) minHeap.offer(node);
           
        }
        
        //2 出队, 放入新链表, 判空入队
    	//	创建虚拟头结点
        ListNode newHead = new ListNode(-1);
        ListNode cur = newHead;
        
        while(!minHeap.isEmpty()){
            ListNode tmp = minHeap.poll();
            ListNode tmpNext = tmp.next;
            //	放入新链表
            cur.next = tmp;
            cur = cur.next;
            //	非空后继入队
            if(tmpNext != null){
                minHeap.offer(tmpNext);
            }
        }
        
        return newHead.next;
    
    }

  • 归并排序
		public ListNode mergeKLists(ArrayList<ListNode> lists) {
            //进行归并排序
            return mergeSort(lists, 0, lists.size() - 1);
        }
    
        //排序
        public ListNode mergeSort(ArrayList<ListNode> lists, int left, int right){
            //中止条件为: 只有一个节点或者没有节点了
            if(left > right) return null;
            
            if(left == right) return lists.get(left);
            
            //本级目标: 将 mid 分开的左右两区间进行子问题排序, 合并成两张表, 再将两新表进行合并
            //	通过 mid 进行区间划分
            int mid = (left + right) / 2;
            //对下面代码进行优化
			//return merge(mergeSort(lists, left, mid), mergeSort(lists, mid + 1, right));
            ListNode leftNode = mergeSort(lists, left, mid);
            ListNode rightNode = mergeSort(lists, mid + 1, right);
            
            //合并子问题返回的两链表的头结点
            ListNode newNode = merge(leftNode, rightNode);
            
            //返回值: 新合并的链表头结点
            return newNode;
            
            
        }
    
        //递归合并
        public ListNode merge(ListNode list1, ListNode list2){
            if(list1 == null) return list2;
            if(list2 == null) return list1;
            
            if(list1.val <= list2.val){
                list1.next = merge(list1.next, list2);
                return list1;
            }else{
                list2.next = merge(list1, list2.next);
                return list2;
            }
            
        }



5 判断链表中是否有环


判断链表中是否有环

1)思路:

  • 进行空节点 和 单节点的判断
  • 快慢指针 不同速度向后移动, 以此循环
    • 注意条件 fast != null && fast.next != null


2)解法:

  • 双指针
public boolean hasCycle(ListNode head) {
        //解法1: 快慢指针
        //1 判空
        if(head == null || head.next == null) return false;
        
        //2 快慢指针
        ListNode fast = head;
        ListNode slow = head;
        
        
        //3 向后移动
        while(fast != null && fast.next != null){
            fast = fast.next.next;
            slow = slow.next;
            
            if(fast == slow){
                return true;
            }
        }
        
        return false;
    }



6 链表中环的入口结点


链表中环的入口结点

1)思路:

(1)双指针

  • 先通过快慢指针判断是否有环
  • 慢指针从头开始走, 快指针从相遇点走, 一次都走一步 再次相遇为入口点
    • 有环的情况下, 从入口点走到相遇点 和 相遇点走到入口点的距离相等

(2)哈希

  • 使用 HashSet 保存节点, 遇到重复节点直接返回, 直到遇到空节点


2)解法:

  • 快慢指针
public ListNode EntryNodeOfLoop(ListNode pHead) {
        //1 快慢指针找到相遇点
        ListNode tmp = hasCycle(pHead);
       if( tmp == null) return null;
        ListNode fast = tmp;
        ListNode slow = pHead;
        
        //2 慢指针从头开始走, 再次相遇为入口点
         while(fast != slow){
            fast = fast.next;
            slow = slow.next;
        }
        
        return slow;
    }
    
    public ListNode hasCycle(ListNode pHead){
        ListNode fast = pHead;
        ListNode slow = pHead;
        
        while(fast != null && fast.next != null){
            fast = fast.next.next;
            slow = slow.next;
            
            if(fast == slow) return slow;
        }
        
        return null;
    }

  • 哈希
public boolean hasCycle(ListNode head) {
        HashSet<ListNode> set = new HashSet<>();
        
        while(head != null){
            //有重复节点直接返回
            if(set.contains(head)){
                return true;
            }
            
            set.add(head);
            head = head.next;
        }
        
        return false;
        
    }



7 两链表的公共节点


两个链表的第一个公共结点

1)思路:

  • 利用链表公共节点后面等长的特性
  • 思路1: 先求出链表差值,让长链表先走差值步, 在一起走找到公共节点
  • 思路2: 先各自走完自己的链表, 走完后转向另一个链表, 直到找到相等节点(为 null 也可以返回)


2)解法:

  • 差值法1
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
         //1 求两链表长度差
         ListNode cur1 = pHead1;
         ListNode cur2 = pHead2;
         int maxLen = 0;
         int minLen = 0;

         while(cur1 != null){
             maxLen++;
             cur1 = cur1.next;
         }

         while(cur2 != null){
             minLen++;
             cur2 = cur2.next;
         }


         //2 长链表先走差值步
         cur1 = pHead1;
         cur2 = pHead2;
         if(maxLen < minLen){
             cur1 = pHead2;
             cur2 = pHead1;
         }

         int sub = Math.abs(maxLen - minLen);
         while(sub > 0){
             cur1 = cur1.next;
             sub--;
         }

  • 差值法2
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
            ListNode cur1 = pHead1;
            ListNode cur2 = pHead2;

            while(cur1 != cur2){
                cur1 =  cur1 == null ? pHead2 : cur1.next;
                cur2 =  cur2 == null ? pHead1 : cur2.next;
            }

            return cur1;

        }



8 链表相加(二)


链表相加(二)

1)思路:

(1)反转链表法:

图示

链表相加
  • 先反转两条链表
  • 创建虚拟头结点, 遍历反转后的双链表, 计算节点和, 创建节点保存到新链表中, 直到链表都为空
    • 使用 三目表达式计算 每个节点的值 和 下一个节点
    • 使用 一个 tmp 先计算节点和, 新节点添加完成后, 计算进位值进行保存
  • 判断进位是否为空, 添加节点,
  • 重新新反转链表 进行返回


2)解法:

public ListNode addInList (ListNode head1, ListNode head2) {
            ListNode cur1 = reverse(head1);
            ListNode cur2 = reverse(head2);
            ListNode dummy = new ListNode(-1);
            ListNode cur = dummy;
            
            //用于保存每个节点值 和 进位值
            int tmp = 0;
            while(cur1 != null || cur2 != null){
                int val1 =  cur1 == null ? 0 : cur1.val;
                int val2 =  cur2 == null ? 0 : cur2.val;
                
                //计算值,保存节点
                tmp = val1 + val2 + tmp;
                ListNode node = new ListNode(tmp % 10);
                cur.next = node;
                cur = cur.next;
                
                //保存进位
                tmp /= 10; 
                
                cur1 =  cur1 == null ? null : cur1.next;
                cur2 =  cur2 == null ? null : cur2.next;
            }
            
    		//判断进位是否为空
            if(tmp != 0){
                cur.next = new ListNode(tmp);
            }
            
            //最后再次反转
            return reverse(dummy.next);
            
        
        }
    
        //反转链表
        public ListNode reverse(ListNode root){
            ListNode pre = null;
            ListNode rootNext = null;
            
            while(root != null){
                rootNext = root.next;
                root.next = pre;
                pre = root;
                root = rootNext;
            }
            
            return pre;
        }



9 单链表的排序


单链表的排序

1)思路:

(1)归并排序:

排序方法传入的 tail 参数为 null, 该节点会在 递的过程中被省略

  • 中止条件: 没有元素 / 只有两个元素
    • 子问题只有两个元素时, 由于本题使用 现有快慢指针 的特征, 必须将后一个元素进省略
    • 但同时该节点未被舍弃, 相邻子问题头结点就是这个被舍弃的节点, 最后只有最后一个 tail 节点被省略, 但是 tail 又为空, 所以不影响结果
  • 本级目标: 拼接两个子问题返回的排序过的新链表
  • 返回值: 返回拼接后链表的头结点

(2)数组排序:

  • 使用 ArrayList 保存链表节点值,
  • 使用 Collection.sort() 进行排序
  • 将值放回链表


2)解法:

  • 归并排序
 	public ListNode sortInList (ListNode head) {
        // write code here
        //尾节点传入 null, 因为进行双指针二分过程中. 最后一个节点必定会被省略
        return sort(head, null);
    }
    
    public ListNode sort(ListNode head, ListNode tail){
        //中止条件 没有元素 / 只有两个元素
        if(head == null)return head;
        
        //只有两个元素是会舍弃后一个元素, 但是在相邻子问题头结点就是这个被舍弃的节点
        //如此迭代, 原链表的最后一个 tail 节点会舍弃, 但是 tail 节点又为空所以没有影响
        if(head.next == tail){
            //如果要进行链表合并 子链表势必是经过调整后的新链表, 
            //如果next 带有原本旧链表的节点, 合并过程就无法顺利进行, 所以将 next置为空
            head.next = null;
            return head;
        }
        
        //本级目标: 拼接两个子问题返回的排序过的新链表(逻辑上不被旧链表影响)
        // 快慢指针进行二分, 快指针到最后一个节点为止 slow 即是mid
        ListNode fast = head;
        ListNode mid = head;
        while(fast != tail){
            fast = fast.next;
            mid = mid.next;
            
            if(fast != tail){
                fast = fast.next;
            }
        }
        
        //返回值: 返回拼接后链表的头结点
        return merge(sort(head, mid), sort(mid, tail));
        
//         ListNode left = sort(head, mid);
//         ListNode right = sort(mid, tail);
//         ListNode node = merge(left, right);
//         return node;
        
    }
    
    public ListNode merge(ListNode list1, ListNode list2){
        if(list1 == null) return list2;
        if(list2 == null) return list1;
        
        if(list1.val <= list2.val){
            list1.next = merge(list1.next, list2);
            return list1;
        }else{
            list2.next = merge(list1, list2.next);
            return list2;
        }
    }

  • 数组排序
public ListNode sortInList (ListNode head) {
        ArrayList<Integer> array = new ArrayList<>();
        ListNode cur = head;
        
        while(cur != null){
            array.add(cur.val);
            cur = cur.next;
        }
        
        Collections.sort(array);
        
        cur = head;
        
        for(int i = 0; i < array.size(); i++){
            cur.val = array.get(i);
            cur = cur.next;
        }
        
        return head;
        
    }



10 链表奇数偶数重排


链表的奇偶重排

1)思路:

(1)双指针法

图示

1 (2)
  • 初始化 奇数的指针 odd 和 偶数指针 end, 同时 保存偶数链表首节点 evenHead
    • evenHead 最后要和 奇数链表进行拼接
  • 连接过程中, 先进行奇数的连接, 再进行偶数的连接
    • 进入循环, 奇数总在偶数之前
    • odd.next = even.next 奇数指向偶数下一个节点, 再后移
    • even.next = odd.next 偶数指向奇数下一个节点, 再后移
    • 对于奇数链表来说, 无论节点个数是 奇 偶 个, 最后一次循环 奇 数节点都能指向带值的有效节点, 供后续进行拼接
    • 对于偶数链表来说, 节点奇数个时, 最后一次循环 偶数节点指向空节点; 节点为偶数个时, 最后一次循环 偶数节点指向原链表的最后一个节点, 最后节点下一个还是为空节点, 所以 偶数链表最后始终为空节点
  • 奇数偶数链表进行拼接, 返回 head
    • 非空奇数节点指向 偶数节点头结点 evenHead


2)解法:

  • 双指针
	public ListNode oddEvenList (ListNode head) {
        // write code 
        //1 判空 初始化 odd even evenHead
        if(head == null) return head;
        ListNode odd = head;
        ListNode even = head.next;
        ListNode evenHead = even;
        //2 进行奇数偶数链表的分别连接
        while(even != null && even.next != null){
            //奇数再偶数前面, 先进行奇数连接
            odd.next = even.next;
            odd = odd.next;
            //再进行偶数连接
            even.next = odd.next;
            even = even.next;
        }
        //3 奇数偶数链表进行拼接 返回 head
        odd.next = evenHead;
        return head;
    }



文章至此就结束啦,如果看官朋友觉得还不错,博主求 点赞、评论、收藏 三连,十分感谢
奇数总在偶数之前

  • odd.next = even.next 奇数指向偶数下一个节点, 再后移
  • even.next = odd.next 偶数指向奇数下一个节点, 再后移
  • 对于奇数链表来说, 无论节点个数是 奇 偶 个, 最后一次循环 奇 数节点都能指向带值的有效节点, 供后续进行拼接
  • 对于偶数链表来说, 节点奇数个时, 最后一次循环 偶数节点指向空节点; 节点为偶数个时, 最后一次循环 偶数节点指向原链表的最后一个节点, 最后节点下一个还是为空节点, 所以 偶数链表最后始终为空节点
  • 奇数偶数链表进行拼接, 返回 head
    • 非空奇数节点指向 偶数节点头结点 evenHead


2)解法:

  • 双指针
	public ListNode oddEvenList (ListNode head) {
        // write code 
        //1 判空 初始化 odd even evenHead
        if(head == null) return head;
        ListNode odd = head;
        ListNode even = head.next;
        ListNode evenHead = even;
        //2 进行奇数偶数链表的分别连接
        while(even != null && even.next != null){
            //奇数再偶数前面, 先进行奇数连接
            odd.next = even.next;
            odd = odd.next;
            //再进行偶数连接
            even.next = odd.next;
            even = even.next;
        }
        //3 奇数偶数链表进行拼接 返回 head
        odd.next = evenHead;
        return head;
    }



文章至此就结束啦,如果看官朋友觉得还不错,博主求 点赞、评论、收藏 三连,十分感谢

本文含有隐藏内容,请 开通VIP 后查看