算法与数据体系课笔记之- 9.2链表相关题目(进行中)

9.链表相关题目分析 总览

笔记思维导图链接

算法与数据结构思维导图

参考左程云体系算法课程笔记
参考慕课网算法体系课程笔记

常见题目汇总:

1. 求链表中间节点

题意:

设计寻找链表指定节点,实现以下四种功能:

  • 1.找到链表的中间节点,若为偶数,找floor,即向下取整,小于中间分界线的数
  • 2.找到链表的中间节点,若为偶数,找ceil, 即向上取整,大于中间分界线的数
  • 3.找到链表的中间节点前一个节点,若为偶数,找floor, 即向下取整,小于中间节点的数
  • 4.找到链表的中间节点前一个节点,若为偶数,找ceil, 即向上取整,大于中间节点的数

题解:

  • 使用快慢指针,快指针的步数是慢指针步数的两倍,快指针走完后,慢指针落在中间附件
  • 对于中间节点floor,ceil,pre的确定,
    • 只需调整快慢指针起始位置,慢指针决定是floor或是ceil
    • 快慢指针相对位置,决定慢指针在中间前一个位置还是中间位置
  • 要注意边界条件,避免空指针错误,对少于三个节点的特殊情况特殊处理

代码实现:

1.找中间值的floor,即向下取整

  • 定义快慢指针,慢指针从起始位置开始,就是向下取整
    要落在中间位置,快慢位置起始位置要一致

  • 	// 1.找中间值的floor,即向下取整
    	public static Node floorMid(Node head) {
    		// 先处理节点个数少于3个的情况的情况
    		if (head == null || head.next == null || head.next.next == null) {
    			return head;
    		}
    		// 定义快慢指针,慢指针从起始位置开始,就是向下取整
    		// 要落在中间位置,快慢位置起始位置要一致
    		Node slow = head;
    		Node fast = head;
    		while (fast.next != null && fast.next.next != null) {
    			slow = slow.next;
    			fast = fast.next.next;
    		}
    		return slow;
    	}
    

2.找中间值的ceil,即向上取整

  • 定义快慢指针,慢指针先走一步,这样可以找到中间值的向上取整

  • 要落在中间位置,快慢位置起始位置要一致

  • 	// 2.找中间值的ceil,即向上取整
    	public static Node ceilMid(Node head) {
    		// 先处理节点个数少于3个的情况的情况
    		if (head == null || head.next == null || head.next.next == null) {
    			return head;
    		}
    		// 定义快慢指针,慢指针先走一步,这样可以找到中间值的向上取整
    		// 要落在中间位置,快慢位置起始位置要一致
    		Node slow = head.next;
    		Node fast = head.next;
    		while (fast.next != null && fast.next.next != null) {
    			slow = slow.next;
    			fast = fast.next.next;
    		}
    		return slow;
    	}
    

3.找中间值floor的前一个节点,即向下取整

  • 定义快慢指针,快指针先多走两步,这样慢指针会相对与中间值慢一步

  • 即快慢指针起始位置相对差两步,相当于慢指针少走一步,来到中间值前一个位置

  • 中间值的floor,同样慢指针从起始位置开始即可

  • 	// 3.找中间值floor的前一个节点,即向下取整
    	public static Node preFloorMid(Node head) {
    		// 先处理节点个数<=2的情况的情况
    		if (head == null || head.next == null || head.next.next == null) {
    			return null;
    		}
    		// 定义快慢指针,快指针先多走两步,这样慢指针会相对与中间值慢一步
    		// 即快慢指针起始位置相对差两步,相当于慢指针一步
    		Node slow = head;
    		Node fast = slow.next.next;
    		while (fast.next != null && fast.next.next != null) {
    			slow = slow.next;
    			fast = fast.next.next;
    		}
    		return slow;
    	}
    

4.找中间值ceil的前一个节点,即向上取整

  • 定义快慢指针,快指针先多走两步,这样慢指针会相对与中间值慢一步

  • 为了向上取整,与ceilMid类似,慢指针先相对起始位置走一步

  • 快指针与慢指针相对位置要差两步,是完整的一个慢指针一步

  • // 4.找中间值ceil的前一个节点,即向上取整
    	public static Node preCeilMid(Node head) {
    		// 先处理节点个数<=2的情况的情况
    		if (head == null || head.next == null || head.next.next == null) {
    			return null;
    		}
    		// 定义快慢指针,快指针先多走两步,这样慢指针会相对与中间值慢一步
    		// 为了向上取整,慢指针需要进一步,为了保证慢指针在中间值前一步,
    		// 快指针与慢指针相对位置要差两步,是完整的一个慢指针一步
    		Node slow = head.next;
    		Node fast = slow.next.next;
    		while (fast.next != null && fast.next.next != null) {
    			slow = slow.next;
    			fast = fast.next.next;
    		}
    		return slow;
    	}
    

代码测试:

0->1->2->3->4->5->6->7

floorMid
3
3
ceilMid
4
4
floorPreMid
2
2
ceilPreMid
3
3

0->1->2->3->4->5->6->7->8


floorMid
4
4
ceilMid
4
4
floorPreMid
3
3
ceilPreMid
3
3

复杂度分析:

  • 链表的遍历,时间都是O(n)
  • 都是原地处理,空间复杂度都是O(1)

2. 回文链表

题目链接

题意:

给定一个链表的 头节点 head **,**请判断其是否为回文链表。

如果一个链表是回文,那么链表节点序列从前往后看和从后往前看是相同的。

例如:

在这里插入图片描述

输入: head = [1,2,3,3,2,1]
输出: true

题解:

解法一:使用额外空间O(n)的容器栈

  • 由于回文链表结构对称,栈后进先出的特性,出栈后元素倒序
  • 判断倒序后的结构与原结构是否相等,即可判断是否为回文链表

解法二:快慢指针,改变链表结构方便比较,空间O(1)

  • 1.先用快慢指针法,找到中间节点的位置
  • 2.将中间节点往右的所有节点进行反转,改成从右指向左
  • 3.从链表的两端向中间依次遍历比较,如果节点不同,就结束循环,得出判断
  • 4.注意是结束循环,不是直接return结果,因为在结束程序前,要将原链表恢复

代码实现:

解法一:用栈

	// 解法一: 用栈容器,回文链表入栈出栈顺序一致
	public boolean isPalindrome(ListNode head) {
		Stack<ListNode> stack = new Stack<>();
		// 1. 依次遍历链表,将节点入栈
		ListNode cur = head;
		while(cur != null) {
			stack.push(cur);
			cur = cur.next;
		}
		
		// 2. 依次将节点弹出栈,并与原链表结构进行比对验证
		cur = head;
		while(!stack.isEmpty()) {
			if(cur.val != stack.pop().val) return false;
			cur = cur.next;
		}
		return true;
	}

解法二:用快慢指针,改变链表结构

//	解法二:快慢指针,改变链表结构方便比较,空间O(1)
	public boolean isPalindrome2(ListNode head) {
		if(head == null || head.next == null) return true;
//	- 1.先用快慢指针法,找到中间节点的位置
		ListNode mid = head;
		ListNode right = head;
		while(right.next !=null && right.next.next != null) {
			mid = mid.next;	// mid最终指向中间节点
			right = right.next.next;
		}
			
//	- 2.将中间节点往右的所有节点进行反转,改成从右指向左
		// mid->cur->right ==> null<-mid<-cur<-right
		ListNode cur = mid.next; // 右边第一个节点
		mid.next = null;  // 改变链表结构,使得中间节点指向null
		while(cur != null) {  // 将中间节点往右的链表结构进行反转
			right = cur.next;
			cur.next = mid;
			mid = cur;        // 最终mid指向链表最后一个节点位置
			cur = right;   
		}
		
//	- 3.从链表的两端向中间依次遍历比较,如果节点不同,就结束循环,得出判断
		cur = mid; // 记录下最后一个节点位置,为了最后一步将链表恢复
		boolean res = true;
		while(head != null && mid != null) {
			if(head.val != mid.val) {
				res = false;
				break;
			}
			head = head.next;
			mid = mid.next;
		}
		
//	- 4.注意是结束循环,不是直接return结果,因为在结束程序前,要将原链表恢复
		// left<-mid<-cur ==> left->mid->cur->null
		mid = cur.next;
		cur.next = null;
		while(mid != null) {
			ListNode left = mid.next;
			mid.next = cur;
			cur = mid;
			mid = left;
		}
		
		return res;
	}

复杂度分析:

  • 时间复杂度都是O(n)
  • 用栈,额外空间复杂度O(n),用快慢指针,空间O(1)

3. 分隔重排链表

题意:

  • 将一个链表,根据传入的数V,将链表节点值分为小于V,等于V,大于V三部分
  • 按照小于V,等于V,大于V的顺序,将链表重新组合成新的链表

题解:

解法一:使用数组容器法
  • 1.统计链表中节点个数,创建对应大小的数组容器
  • 2.再遍历一次,将链表节点加入数组容器中
  • 3.对数组元素进行快速三路排序法,分成三个区域
  • 4.将排好序的数组元素串成链表,即最终需要的链表结构
解法二:使用指针对链表原地进行排序
  • 1.用6个指针,分别指向三个区域的头尾节点,遍历一遍链表,即可将链表分割好
  • 2.判断验证每个节点,与v大小比较,放入指定区间前,
    • 要将当前节点的next断掉,并新建节点保存要处理的下个节点
  • 3.最后将三个链表进行拼接
    • 小区间的尾部连接等于区间的头部,等于区间的尾部连接大于区间的头部
    • 注意边界条件,即小于区间,等于区间是否为空,要做特殊处理

代码实现:

解法一:使用数组快排
	// 解法一:使用容器,将链表节点装入数组中,进行排序,再串成链表
	public static Node listPartition1(Node head, int pivot) {
		if(head == null) return null;
//		- 1.统计链表中节点个数,创建对应大小的数组容器
		int size = 0;
		Node cur = head;
		while(cur != null) {
			size ++;
			cur = cur.next;
		}
		Node[] arr = new Node[size];
		
//		- 2.再遍历一次,将链表节点加入数组容器中
		int i = 0;
		cur = head;
		for(i = 0; i < arr.length; i ++) {
			arr[i] = cur;
			cur = cur.next;
		}
		
//		- 3.对数组元素进行快速三路排序法,分成三个区域(一次就行,每个区域无需排序)
		partition(arr, pivot);
		
//		- 4.将排好序的数组元素串成链表,即最终需要的链表结构
		for(i = 1; i < arr.length; i ++) {
			arr[i - 1].next = arr[i];
		}
		arr[i - 1].next = null;
		return arr[0];
	}

	private static void partition(Node[] arr, int pivot) {
		// 1.先定义好指针,划分好三个区间
		// [0, pL]<v, [pL + 1, pR - 1]=v,[pR,n - 1]>v
		int pL = -1; // pL指向小于v的数
		int i = 0;   // i指向等于v的数
		int pR = arr.length; // pR指向大于v的数
		
		// 2.i依次遍历,将元素放到指定区间内
		while(i < pR) {
			if(arr[i].value < pivot) {
				swap(arr, i ++, ++pL); // 先开辟空间,移动指针到正确位置,再放值
			} else if(arr[i].value > pivot) {
				swap(arr, i, --pR); // 从右边交换过来的数,要继续判断验证
			} else {
				i ++;
			}
		}
	}

	private static void swap(Node[] arr, int i, int j) {
		Node t = arr[i];
		arr[i] = arr[j];
		arr[j] = t;
	}
解法二:使用多指针法原地分割链表
// 解法二:使用多个指针确定三个区间,进行分割,再拼接
	public static Node listPartition2(Node head, int pivot) {
		if(head == null) return null;
//	- 1.用6个指针,分别指向三个区域的头尾节点,遍历一遍链表,即可将链表分割好
		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		
		
//	- 2.判断验证每个节点,与v大小比较,放入指定区间前,
//	    - 要将当前节点的next断掉,并新建节点保存要处理的下个节点
		Node cur = head;  // 要处理的节点
		Node next = null; // 保存要处理节点的下个节点
		while(cur != null) {
			next = cur.next;
			cur.next = null; // 要将要处理的节点next断掉,避免一个节点被多个节点的next指向
			if(cur.value < pivot) {
				if(sH == null) {
					sH = cur;
					sT = cur;
				} else {
					sT.next = cur;
					sT = cur;
				}
			} else if(cur.value == pivot) {
				if(eH == null) {
					eH = cur;
					eT = cur;
				} else {
					eT.next = cur;
					eT = cur;
				}
			} else {
				if(mH == null) {
					mH = cur;
					mT = cur;
				} else {
					mT.next = cur;
					mT = cur;
				}
			}
			cur = next;
		}
		
//	- 3.最后将三个链表进行拼接
//	    - 小区间的尾部连接等于区间的头部,等于区间的尾部连接大于区间的头部
//	    - 注意边界条件,即等于区间,大于区间是否为空,要做特殊处理
		// 先处理小于区域或等于区域不全为null的情况,需要将区域相连
		if(sT != null) { // 说明有小于区域,但不一定有等于区域
			sT.next = eH;
			// 判断是否有等于区域,查验eT是否为空即可
			eT = eT == null ? sT : eT; // 下一步,谁去连大于区域的头,谁就变成eT
		} 
		if(eT != null) { // 排除即无小于区域,又无等于区域的情况
			eT.next = mH; // 不管大于区域是否有都行,一样链表是完整的
		}
		// 如果小于区域和等于区域全为null,直接找大于区域头节点即可
		return sH != null ? sH : (eH != null ? eH : mH);
	}

复杂度分析:

  • 时间复杂度都为O(n)
  • 使用数组,额外空间O(n), 使用多指针法,额外空间O(1)

4. 分割链表扩展题目

分割链表扩展题目链接

题意:

  • 给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,

    • 使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。
  • 你应当 保留 两个分区中每个节点的初始相对位置

例如:

在这里插入图片描述
输入:head = [1,4,3,2,5,2], x = 3
输出:[1,2,2,4,3,5]

题解:

  • 因要保证每个节点的初始相对位置不变,用容器进行快排很难达到要求
  • 因此必须使用多个指针,原地分割链表的方法,与上题类似,
  • 先划分好要分割的区间,再将节点放入指定区间,最后将两个区间相连即可

代码实现

public ListNode partition(ListNode head, int x) {
        if(head == null) return null;
        // 1. 定义指针,划分两个区间
        ListNode smallHead = null;
        ListNode smallTail = null;
        ListNode rightHead = null;
        ListNode rigthTail = null;
        // 2. 将链表节点按照要求分割在两个区间内
        ListNode cur = head;
        ListNode next = null;
        while(cur != null) {
            next = cur.next;
            cur.next = null;
            if(cur.val < x) {
                if(smallHead == null) {
                    smallHead = cur;
                    smallTail = cur;
                } else {
                    smallTail.next = cur;
                    smallTail = cur;
                }
            } else {
                if(rightHead == null) {
                    rightHead = cur;
                    rigthTail = cur;
                } else {
                    rigthTail.next = cur;
                    rigthTail = cur;
                }
            }
            cur = next;
        }
        // 3. 将两个区间的链表进行拼接
        if(smallTail != null) {
            smallTail.next = rightHead;
        }
        return smallHead != null ? smallHead : rightHead;
    }

复杂度:

  • 时间O(n), 空间O(1)

5. 复制带有随机指针的链表

题目链接

题意:

  • 实现 copyRandomList 函数,复制一个复杂链表。
  • 在复杂链表中,每个节点除了有一个 next 指针指向下一个节点,
  • 还有一个 random 指针指向链表中的任意节点或者 null

例如:

在这里插入图片描述

输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]

题解:

解法一:使用容器,map
  • 1.使用map容器,使用map映射,就是影子工程
  • 2.添加键值对,旧节点映射新节点,key为旧节点,value为新节点
  • 3.添加新链表节点的next。random,参考旧链表对应节点的next和random
解法二:不使用容器,原地处理
  • 1.为了节省空间,在每个节点后面新建一个影子节点,与map映射作用类似
  • 2.使用旧节点的映射来处理新节点的random指针
  • 3.最后将新旧节点断开,next方向上,把新老链表分离

代码实现:

解法一:使用容器,map
// 解法一:使用map容器进行映射
	public Node copyRandomList(Node head) {
		if(head == null) return null;
//		- 1.使用map容器,使用map映射,就是影子工程
		Map<Node, Node> map = new HashMap<>();
		
//		- 2.添加键值对,旧节点映射新节点,key为旧节点,value为新节点
		 Node cur = head;
		 while(cur != null) {
			 map.put(cur, new Node(cur.val));
			 cur = cur.next;
		 }
		
//		- 3.添加新链表节点的next。random,参考旧链表对应节点的next和random
		cur = head;
		while(cur != null) {
			map.get(cur).next = map.get(cur.next);
			map.get(cur).random = map.get(cur.random);
			cur = cur.next;
		}
		return map.get(head);
	}
解法二:使用另一个指针维护新的链表
  // 解法二:使用指针,对链表原地处理
	public Node copyRandomList(Node head) {
		if(head == null) return null;
//		- 1.先创建影子队伍
//		为了节省空间,在每个节点后面新建一个影子节点,与map映射作用类似
		Node pit1 = head;
		Node pit2 = null; // 影子节点
		// 1 -> 2 -> 3 -> null
		// 1 -> 1' -> 2 -> 2' -> 3 -> 3'
		while(pit1 != null) {
			pit2 = new Node(pit1.val); // 新建节点
			
			pit2.next = pit1.next;      // 将新健的节点加入链表中
			pit1.next = pit2;   
			
			pit1 = pit1.next.next;
		}
		
//		- 2.组建好影子队伍的random指针
//		使用旧节点的映射来处理新节点的random指针
		pit1 = head;
		while(pit1 != null) {
			pit2 = pit1.next;
			// 要注意旧链表的random是否为null,否则random.next空指针异常
			pit2.random = pit1.random != null ? pit1.random.next : null;
			
			pit1 = pit1.next.next; // 继续找下一个旧节点
		}
		
//		- 3.脱离原队伍,自己带队
//		最后将新旧节点断开,处理next指针
		pit1 = head;
		Node res = pit1.next; // 保留好新链表的头节点,作为返回的结果
		while(pit1 != null) {
			pit2 = pit1.next;
			pit1.next = pit1.next.next;     // 改变旧节点的next指针
			
			// 改变新节点的next指针,指向下个影子节点
			// 同样,要处理新节点为最后一个节点的情况,空指针异常
			pit2.next = pit2.next != null ? pit2.next.next : null; 
			
			pit1 = pit1.next;
		}
		
		return res;
	}

复杂度分析:

  • 时间复杂度都是O(n)
  • 使用容器,空间O(n),不使用容器O(1)

6. 链表中环的入口节点

题目链接

题意

  • 给定一个链表,返回链表开始入环的第一个节点

  • 从链表的头节点开始沿着 next 指针进入环的第一个节点为环的入口节点。如果链表无环,则返回 null

  • 为了表示给定链表中的环,使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。

    • 即环的入口节点位置索引
    • 如果 pos 是 -1,则在该链表中没有环。
    • 注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。
在这里插入图片描述
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。

题解

解法一:使用容器set记录节点
  • 1.使用集合set记录每个节点,包括节点的val和next指针
  • 2.依次遍历记录中,如果发现有遍历过的节点,该节点就是环的入口
  • 3.如果一直遍历到结束,说明没有环
解法二:使用快慢指针找到入口节点
fig1
  • 1.设计快指针fast,慢指针slow,快指针每次走两步,慢走一步

  • 2.首先,如果没环,fast会一直走到底,如果有环,一定会与slow相遇,假设相遇时slow进入环b个节点

    • 一定相遇原因,slow进入环前,fast一直在环内打转,

      • 当slow进入环后,fast一定会绕到slow后面
      • 每走一步,fast与slow差距缩短一步,直到差距缩短为零,相遇
    • 相遇时,fast走的总路程是slow走的总路程的两倍

      假设只走了一圈:

      • fast走的路程是: a + b + c+ b

      • slow走的路程是:a + b

      • fast = 2(slow)–>a + b + c+ b = a+ b + a + b --> c = a

        假设走了n圈:

      • fast走了f,slow走了s,有f=s+n(b + c),f=2s==>s=n(b + c)

      • 从头到环入口,走的路径是:a+n(b + c), 由于s=n(b + c),故,slow再走a即可到达环入口

      • 为了统计a,再用双指针,从头部与slow同时出发,头到入口正好为a,即再次相遇时,两者都走了a

  • 3.通过推论知,a与c相等,即再让指针pit从起点与slow同速度走,一定在环的入口相遇

代码实现

解法一:使用容器set记录节点
// 解法一:使用容器法
	public ListNode detectCycle(ListNode head) {
		if(head == null) return null;
		
//		- 1.使用集合set记录每个节点,包括节点的val和next指针
		ListNode cur = head;
		Set<ListNode> set = new HashSet<>();
		
//		- 2.依次遍历记录中,如果发现有遍历过的节点,该节点就是环的入口
		while(cur != null) {
			if(!set.contains(cur)) {
				set.add(cur);
			} else {
				return cur;
			}
			cur = cur.next;
		}
		
//		- 3.如果一直遍历到结束,说明没有环
		return null;
	}
解法二:双指针法:
// 双指针法
	// 链表头到环入口处节点数为a,环中节点数为(b + c),slow进入环后再走b与fast相遇
	// fast走了f,slow走了s,有f=s+n(b + c),f=2s==>s=n(b + c)
	// 从头到环入口,a+n(b + c), 由于s=n(b + c),故,slow再走a即可到达环入口
	// 为了统计a,再用双指针,从头部与slow同时出发,头到入口正好为a,即再次相遇时,两者都走了a
	public ListNode detectCycle(ListNode head) {
		if(head == null) return null;
		
//		- 1.设计快指针fast,慢指针slow,快指针每次走两步,慢走一步
		ListNode fast = head;
		ListNode slow = head;
		
//		- 2.首先,如果没环,fast会一直走到底,如果有环,一定会与slow相遇,
		while(true) {
			if(fast.next == null || fast.next.next == null) {
				return null;  // 能走到底,尾节点指向null,说明没环
			}
			slow = slow.next;
			fast = fast.next.next; // 有环的话,一定会相遇,
            if(fast == slow) break;
		}
		
//		- 3.通过推论知,再让指针pit从起点与slow同速度走,一定在环的入口相遇
		fast = head;
		while(fast != slow) {
			fast = fast.next;
			slow = slow.next;
		}
		
		return fast;
	}

复杂度分析

  • 时间都是O(n)
  • 空间,使用容器,O(n), 不使用容器O(1)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值