【算法笔记】链表相关的题目
单向链表是非常考验编码水平的一类题目,很多时候感觉很简单的算法,在指针上一不注意,就会出现各种意想不到的问题,所以对于这类题目,要思路清晰,定义好变量和之间的关系。今天梳理几个链表相关的题目。
1、链表中间节点问题
链表中间节点问题
- 快慢指针:
- 快指针一次走两步,慢指针一次走一步,当快指针到达链表末尾时,慢指针指向的位置就是中点
- 1)输入链表头节点,奇数长度返回中点,偶数长度返回上中点
- 2)输入链表头节点,奇数长度返回中点,偶数长度返回下中点
- 3)输入链表头节点,奇数长度返回中点前一个,偶数长度返回上中点前一个
- 4)输入链表头节点,奇数长度返回中点前一个,偶数长度返回下中点前一个
- 思路:
- 如果快慢指针一开始是从同一个节点出发,快指针一次走两步,慢指针一次走一步,当快指针到达链表末尾时,慢指针指向的位置就是奇数长度中点,偶数长度上中点,
- 如果要进行下中点或者前一个节点的判断,只需要让让快慢指针起始点做对应的调整即可
1.1、输入链表头节点,奇数长度返回中点,偶数长度返回上中点
/*** 奇数长度返回中点,偶数长度返回上中点* 如果快慢指针一开始是从同一个节点出发,快指针一次走两步,慢指针一次走一步,* 当快指针到达链表末尾时,慢指针指向的位置就是奇数长度中点,偶数长度上中点*/public static Node midOrUpMidNode(Node head) {if (head == null || head.next == null || head.next.next == null) {// 一个节点或者两个节点,返回的都是head位置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;}
1.2、输入链表头节点,奇数长度返回中点,偶数长度返回下中点
/*** 奇数长度返回中点,偶数长度返回下中点* 思路:* 快慢指针同时出发的时候,走到结尾,慢指针指向的是上中点,* 如果让快慢指针都走到head.next节点,即一开始,快指针少走了一步,* 当快指针到达链表末尾时,慢指针指向的位置就是下中点*/public static Node midOrDownMidNode(Node head) {if (head == null || head.next == null) {return head;}// 让快慢指针一开始同时走到head.next,即一开始的时候,快指针少走了一步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;}
1.3、输入链表头节点,奇数长度返回中点前一个,偶数长度返回上中点前一个
/*** 奇数长度返回中点前一个,偶数长度返回上中点前一个* 思路:* 快慢指针同时出发的时候,走到结尾,慢指针指向的是上中点,* 现在是要求上中点的前一个,只能让慢指针一开始少走一步,反过来就是让快指针多走一步,* 当快指针到达链表末尾时,慢指针指向的位置就是上中点的前一个*/public static Node midOrUpMidPreNode(Node head) {if (head == null || head.next == null || head.next.next == null) {return null;}// 慢指针没有走,快指针走了一步,所以慢指针指向的是head,快指针指向的是head.nextNode slow = head;Node fast = head.next.next;while (fast.next != null && fast.next.next != null) {slow = slow.next;fast = fast.next.next;}return slow;}
1.4、输入链表头节点,奇数长度返回中点前一个,偶数长度返回下中点前一个
/*** 奇数长度返回中点前一个,偶数长度返回下中点前一个* 思路:* 在求下中点的时候,我们让快慢指针都走到head.next节点,然后求出的就是下中点,* 要求前一个,要让慢指针少走一步,所以一开始让慢指针不动,快指针走一步即可。* 偶数情况和求上中点一样,奇数情况下不同*/public static Node midOrDownMidPreNode(Node head) {if (head == null || head.next == null) {return null;}if (head.next.next == null) {return head;}Node slow = head;Node fast = head.next;while (fast.next != null && fast.next.next != null) {slow = slow.next;fast = fast.next.next;}return slow;}
链表中间节点全部代码和对数器测试代码如下:
import java.util.ArrayList;/*** 链表中间节点问题* 快慢指针:* 快指针一次走两步,慢指针一次走一步,当快指针到达链表末尾时,慢指针指向的位置就是中点* 1)输入链表头节点,奇数长度返回中点,偶数长度返回上中点* 2)输入链表头节点,奇数长度返回中点,偶数长度返回下中点* 3)输入链表头节点,奇数长度返回中点前一个,偶数长度返回上中点前一个* 4)输入链表头节点,奇数长度返回中点前一个,偶数长度返回下中点前一个* 思路:* 如果快慢指针一开始是从同一个节点出发,快指针一次走两步,慢指针一次走一步,当快指针到达链表末尾时,慢指针指向的位置就是奇数长度中点,偶数长度上中点,* 如果要进行下中点或者前一个节点的判断,只需要让让快慢指针起始点做对应的调整即可*/
public class LinkedListMidNode {/*** 链表节点结构体*/public static class Node {private final int value;private Node next;public Node(int value) {this.value = value;}}/*** 奇数长度返回中点,偶数长度返回上中点* 如果快慢指针一开始是从同一个节点出发,快指针一次走两步,慢指针一次走一步,* 当快指针到达链表末尾时,慢指针指向的位置就是奇数长度中点,偶数长度上中点*/public static Node midOrUpMidNode(Node head) {if (head == null || head.next == null || head.next.next == null) {// 一个节点或者两个节点,返回的都是head位置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;}/*** 奇数长度返回中点,偶数长度返回上中点的对数器* 用一个链表存放所有节点,然后用下表计算出需要的节点,* 因为数组下标是从0开始,所以上节点的位置index=(链表长度-1)/2*/public static Node midOrUpMidNodeComparator(Node head) {if (head == null || head.next == null || head.next.next == null) {return head;}ArrayList<Node> list = new ArrayList<>();Node cur = head;while (cur != null) {list.add(cur);cur = cur.next;}// 根据长度奇偶算出中点的位置int midIndex = list.size() % 2 == 0 ? list.size() / 2 : list.size() / 2 + 1;// 数组下标从0开始,所以需要减1return list.get(midIndex - 1);}/*** 奇数长度返回中点,偶数长度返回下中点* 思路:* 快慢指针同时出发的时候,走到结尾,慢指针指向的是上中点,* 如果让快慢指针都走到head.next节点,即一开始,快指针少走了一步,* 当快指针到达链表末尾时,慢指针指向的位置就是下中点*/public static Node midOrDownMidNode(Node head) {if (head == null || head.next == null) {return head;}// 让快慢指针一开始同时走到head.next,即一开始的时候,快指针少走了一步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;}/*** 奇数长度返回中点,偶数长度返回下中点*/public static Node midOrDownMidNodeComparator(Node head) {if (head == null || head.next == null) {return head;}ArrayList<Node> list = new ArrayList<>();Node cur = head;while (cur != null) {list.add(cur);cur = cur.next;}// 不管是偶数还是奇数,都取中点的下一个int midIndex = list.size() / 2 + 1;// 数组下标从0开始,所以需要减1return list.get(midIndex - 1);}/*** 奇数长度返回中点前一个,偶数长度返回上中点前一个* 思路:* 快慢指针同时出发的时候,走到结尾,慢指针指向的是上中点,* 现在是要求上中点的前一个,只能让慢指针一开始少走一步,反过来就是让快指针多走一步,* 当快指针到达链表末尾时,慢指针指向的位置就是上中点的前一个*/public static Node midOrUpMidPreNode(Node head) {if (head == null || head.next == null || head.next.next == null) {return null;}// 慢指针没有走,快指针走了一步,所以慢指针指向的是head,快指针指向的是head.nextNode slow = head;Node fast = head.next.next;while (fast.next != null && fast.next.next != null) {slow = slow.next;fast = fast.next.next;}return slow;}/*** 奇数长度返回中点前一个,偶数长度返回上中点前一个*/public static Node midOrUpMidPreNodeComparator(Node head) {if (head == null || head.next == null || head.next.next == null) {return null;}ArrayList<Node> list = new ArrayList<>();Node cur = head;while (cur != null) {list.add(cur);cur = cur.next;}// 先求出上中点int midIndex = list.size() % 2 == 0 ? list.size() / 2 : list.size() / 2 + 1;// 数组下标从0开始,所以需要减2return list.get(midIndex - 2);}/*** 奇数长度返回中点前一个,偶数长度返回下中点前一个* 思路:* 在求下中点的时候,我们让快慢指针都走到head.next节点,然后求出的就是下中点,* 要求前一个,要让慢指针少走一步,所以一开始让慢指针不动,快指针走一步即可。* 偶数情况和求上中点一样,奇数情况下不同*/public static Node midOrDownMidPreNode(Node head) {if (head == null || head.next == null) {return null;}if (head.next.next == null) {return head;}Node slow = head;Node fast = head.next;while (fast.next != null && fast.next.next != null) {slow = slow.next;fast = fast.next.next;}return slow;}/*** 奇数长度返回中点前一个,偶数长度返回下中点前一个*/public static Node midOrDownMidPreNodeComparator(Node head) {if (head == null || head.next == null) {return null;}if (head.next.next == null) {return head;}ArrayList<Node> list = new ArrayList<>();Node cur = head;while (cur != null) {list.add(cur);cur = cur.next;}// 偶数就是求上中点,奇数就是中点前一个int midIndex = list.size() / 2;return list.get(midIndex - 1);}public static void main(String[] args) {// 测试次数int testTime = 500000;// 数组最大长度int maxSize = 100;// 数组最大值int maxValue = 100;boolean succeed = true;for (int i = 0; i < testTime; i++) {// 随机生成一个数组Node head = generateRandomLinkedList(maxSize, maxValue);// 上中点Node midOrUpMid = midOrUpMidNode(head);Node midOrUpMidComparator = midOrUpMidNodeComparator(head);if (midOrUpMid != midOrUpMidComparator) {succeed = false;System.out.println("链表");printLinkedList(head);System.out.println("快慢指针求中点:" + midOrUpMid.value);System.out.println("数组求中点:" + midOrUpMidComparator.value);break;}// 下中点Node midOrDownMid = midOrDownMidNode(head);Node midOrDownMidComparator = midOrDownMidNodeComparator(head);if (midOrDownMid != midOrDownMidComparator) {succeed = false;System.out.println("链表");printLinkedList(head);System.out.println("快慢指针求中点:" + midOrDownMid.value);System.out.println("数组求中点:" + midOrDownMidComparator.value);break;}// 上中点前一个Node midOrUpMidPre = midOrUpMidPreNode(head);Node midOrUpMidPreComparator = midOrUpMidPreNodeComparator(head);if (midOrUpMidPre != midOrUpMidPreComparator) {succeed = false;System.out.println("链表");printLinkedList(head);System.out.println("快慢指针求上中点前一个:" + midOrUpMidPre.value);System.out.println("数组求上中点前一个:" + midOrUpMidPreComparator.value);break;}// 下中点前一个Node midOrDownMidPre = midOrDownMidPreNode(head);Node midOrDownMidPreComparator = midOrDownMidPreNodeComparator(head);if (midOrDownMidPre != midOrDownMidPreComparator) {succeed = false;System.out.println("链表");printLinkedList(head);System.out.println("快慢指针求下中点前一个:" + midOrDownMidPre.value);System.out.println("数组求下中点前一个:" + midOrDownMidPreComparator.value);break;}}System.out.println(succeed ? "successful!" : "error!");}public static Node generateRandomLinkedList(int maxSize, int maxValue) {// 先生成一个数组,然后再转成链表// Math.random() -> [0,1) 所有的小数,等概率返回一个// Math.random() * N -> [0,N) 所有小数,等概率返回一个// (int)(Math.random() * N) -> [0,N-1] 所有的整数,等概率返回一个// (int)(Math.random() * (maxSize + 1)) -> [0,maxSize] 所有的整数,等概率返回一个int size = (int) (Math.random() * (maxSize + 1));int[] arr = new int[size];for (int i = 0; i < arr.length; i++) {arr[i] = (int) (Math.random() * (maxValue + 1)) - (int) (Math.random() * maxValue);}// 转成链表if (size == 0) {return null;}Node head = new Node(arr[0]);Node cur = head;for (int i = 1; i < arr.length; i++) {cur.next = new Node(arr[i]);cur = cur.next;}return head;}public static void printLinkedList(Node head) {Node cur = head;int size = 0;while (cur != null) {size++;System.out.print(cur.value);if (cur.next != null) {System.out.print(" -> ");}cur = cur.next;}System.out.println(" 链表长度为:" + size);}
}
2、判断回文链表问题
判断回文链表问题
- 回文链表:指正读和反读都一样的链表,比如 1->2->3->2->1
- 给定一个单链表的头节点head,请判断该链表是否为回文结构。
- 1)哈希表方法特别简单(笔试用)
- 2)改原链表的方法就需要注意边界了(面试用)
2.1、用栈判断回文链表
/*** 用栈来判断链表是不是回文* 时间复杂度O(n),空间复杂度O(n)* 将链表中的数据历次放入到栈中,然后从头还是遍历,并将其结果与栈中弹出的数据进行对比,如果都相同,代表就是回文链表。* 其本质就是用栈先将数据倒序了,然后在判断顺序和倒序的是否相同*/public static boolean isPalindromeWithStack(Node head) {if (head == null || head.next == null) {return true;}Stack<Node> stack = new Stack<Node>();// 虽然可以直接用head进行遍历,但是为了清晰,还是用单独的遍历比较好,以免混淆Node cur = head;while (cur != null) {stack.push(cur);cur = cur.next;}// 从新遍历链表,每次都从栈中弹出一个节点,然后与当前节点进行对比,如果都相同,代表就是回文链表cur = head;while (cur != null) {if (cur.value != stack.pop().value) {return false;}cur = cur.next;}return true;}
2.2、用栈判断回文链表的改进(节省一半空间)
/*** 用栈的方式判断链表是否为回文链表,但是只使用栈的右半部分* 时间复杂度O(n),空间复杂度O(n/2)* 思路:* 因为回文链表从中心位置开始,两边是对称的,* 这样的话我们只需要将后半部分压入栈中,做逆序处理,就可以与前半部分进行比较,得出结论* 具体实现:* 1)用快慢指针找到链表的中点* 2)将链表的右半部分压入栈中* 3)从栈中弹出节点,与链表的左半部分进行对比*/public static boolean isPalindromeWithRightStack(Node head) {if (head == null || head.next == null) {return true;}// 先用快慢指针找到中间位置Node slow = head;Node fast = head;while (fast.next != null && fast.next.next != null) {slow = slow.next;fast = fast.next.next;}// 此时慢指针所指向的位置就是中间位置,从下个位置开始将右半部分压入栈中Node right = slow.next;Stack<Node> stack = new Stack<Node>();while (right != null) {stack.push(right);right = right.next;}// 从栈中弹出节点,与链表的左半部分进行对比Node cur = head;while (!stack.isEmpty()) {if (cur.value != stack.pop().value) {return false;}cur = cur.next;}return true;}
2.3、反转右侧链表判断是否回文链表
/*** 反转右侧链表判断是否回文链表* 先找到链表的中点,然后将中点后面的节点反转,然后从链表头和尾部开始对比,就可以判断是不是回文* 判断完成以后,要把后面的链表翻转回去,恢复原来的样子* 时间复杂度O(n),空间复杂度O(1)*/public static boolean isPalindromeWithReverseList(Node head) {if (head == null || head.next == null) {return true;}// 先用快慢指针找到中间的位置Node slow = head;Node fast = head;while (fast.next != null && fast.next.next != null) {slow = slow.next;fast = fast.next.next;}// 此时slow指向的位置就是中间的位置,从下一个开始,反转后面的链表Node pre = slow.next;Node cur = pre.next;// 将中间节点的next设置为null,断开链表pre.next = null;Node next = null;while (cur != null) {next = cur.next;cur.next = pre;pre = cur;cur = next;}// 此时的pre就是右侧链表反转后的头结点// 从链表头和pre开始对比,判断是否回文cur = head;Node right = pre;boolean res = true;while (right != null) {if (cur.value != right.value) {res = false;break;}cur = cur.next;right = right.next;}// 最后要把链表恢复原状cur = pre.next;pre.next = null;while (cur != null) {next = cur.next;cur.next = pre;pre = cur;cur = next;}// 接续上链表slow.next = pre;return res;}
完整代码:
import java.util.Stack;/*** 判断回文链表问题* 回文链表:指正读和反读都一样的链表,比如 1->2->3->2->1* 给定一个单链表的头节点head,请判断该链表是否为回文结构。* 1)哈希表方法特别简单(笔试用)* 2)改原链表的方法就需要注意边界了(面试用)*/
public class PalindromeList {/*** 用栈来判断链表是不是回文* 时间复杂度O(n),空间复杂度O(n)* 将链表中的数据历次放入到栈中,然后从头还是遍历,并将其结果与栈中弹出的数据进行对比,如果都相同,代表就是回文链表。* 其本质就是用栈先将数据倒序了,然后在判断顺序和倒序的是否相同*/public static boolean isPalindromeWithStack(Node head) {if (head == null || head.next == null) {return true;}Stack<Node> stack = new Stack<Node>();// 虽然可以直接用head进行遍历,但是为了清晰,还是用单独的遍历比较好,以免混淆Node cur = head;while (cur != null) {stack.push(cur);cur = cur.next;}// 从新遍历链表,每次都从栈中弹出一个节点,然后与当前节点进行对比,如果都相同,代表就是回文链表cur = head;while (cur != null) {if (cur.value != stack.pop().value) {return false;}cur = cur.next;}return true;}/*** 用栈的方式判断链表是否为回文链表,但是只使用栈的右半部分* 时间复杂度O(n),空间复杂度O(n/2)* 思路:* 因为回文链表从中心位置开始,两边是对称的,* 这样的话我们只需要将后半部分压入栈中,做逆序处理,就可以与前半部分进行比较,得出结论* 具体实现:* 1)用快慢指针找到链表的中点* 2)将链表的右半部分压入栈中* 3)从栈中弹出节点,与链表的左半部分进行对比*/public static boolean isPalindromeWithRightStack(Node head) {if (head == null || head.next == null) {return true;}// 先用快慢指针找到中间位置Node slow = head;Node fast = head;while (fast.next != null && fast.next.next != null) {slow = slow.next;fast = fast.next.next;}// 此时慢指针所指向的位置就是中间位置,从下个位置开始将右半部分压入栈中Node right = slow.next;Stack<Node> stack = new Stack<Node>();while (right != null) {stack.push(right);right = right.next;}// 从栈中弹出节点,与链表的左半部分进行对比Node cur = head;while (!stack.isEmpty()) {if (cur.value != stack.pop().value) {return false;}cur = cur.next;}return true;}/*** 反转右侧链表判断是否回文链表* 先找到链表的中点,然后将中点后面的节点反转,然后从链表头和尾部开始对比,就可以判断是不是回文* 判断完成以后,要把后面的链表翻转回去,恢复原来的样子* 时间复杂度O(n),空间复杂度O(1)*/public static boolean isPalindromeWithReverseList(Node head) {if (head == null || head.next == null) {return true;}// 先用快慢指针找到中间的位置Node slow = head;Node fast = head;while (fast.next != null && fast.next.next != null) {slow = slow.next;fast = fast.next.next;}// 此时slow指向的位置就是中间的位置,从下一个开始,反转后面的链表Node pre = slow.next;Node cur = pre.next;// 将中间节点的next设置为null,断开链表pre.next = null;Node next = null;while (cur != null) {next = cur.next;cur.next = pre;pre = cur;cur = next;}// 此时的pre就是右侧链表反转后的头结点// 从链表头和pre开始对比,判断是否回文cur = head;Node right = pre;boolean res = true;while (right != null) {if (cur.value != right.value) {res = false;break;}cur = cur.next;right = right.next;}// 最后要把链表恢复原状cur = pre.next;pre.next = null;while (cur != null) {next = cur.next;cur.next = pre;pre = cur;cur = next;}// 接续上链表slow.next = pre;return res;}public static void main(String[] args) {Node head = null;printLinkedList(head);System.out.print(isPalindromeWithStack(head) + " | ");System.out.print(isPalindromeWithRightStack(head) + " | ");System.out.println(isPalindromeWithReverseList(head) + " | ");printLinkedList(head);System.out.println("=========================");head = new Node(1);printLinkedList(head);System.out.print(isPalindromeWithStack(head) + " | ");System.out.print(isPalindromeWithRightStack(head) + " | ");System.out.println(isPalindromeWithReverseList(head) + " | ");printLinkedList(head);System.out.println("=========================");head = new Node(1);head.next = new Node(2);printLinkedList(head);System.out.print(isPalindromeWithStack(head) + " | ");System.out.print(isPalindromeWithRightStack(head) + " | ");System.out.println(isPalindromeWithReverseList(head) + " | ");printLinkedList(head);System.out.println("=========================");head = new Node(1);head.next = new Node(1);printLinkedList(head);System.out.print(isPalindromeWithStack(head) + " | ");System.out.print(isPalindromeWithRightStack(head) + " | ");System.out.println(isPalindromeWithReverseList(head) + " | ");printLinkedList(head);System.out.println("=========================");head = new Node(1);head.next = new Node(2);head.next.next = new Node(3);printLinkedList(head);System.out.print(isPalindromeWithStack(head) + " | ");System.out.print(isPalindromeWithRightStack(head) + " | ");System.out.println(isPalindromeWithReverseList(head) + " | ");printLinkedList(head);System.out.println("=========================");head = new Node(1);head.next = new Node(2);head.next.next = new Node(1);printLinkedList(head);System.out.print(isPalindromeWithStack(head) + " | ");System.out.print(isPalindromeWithRightStack(head) + " | ");System.out.println(isPalindromeWithReverseList(head) + " | ");printLinkedList(head);System.out.println("=========================");head = new Node(1);head.next = new Node(2);head.next.next = new Node(3);head.next.next.next = new Node(1);printLinkedList(head);System.out.print(isPalindromeWithStack(head) + " | ");System.out.print(isPalindromeWithRightStack(head) + " | ");System.out.println(isPalindromeWithReverseList(head) + " | ");printLinkedList(head);System.out.println("=========================");head = new Node(1);head.next = new Node(2);head.next.next = new Node(2);head.next.next.next = new Node(1);printLinkedList(head);System.out.print(isPalindromeWithStack(head) + " | ");System.out.print(isPalindromeWithRightStack(head) + " | ");System.out.println(isPalindromeWithReverseList(head) + " | ");printLinkedList(head);System.out.println("=========================");head = new Node(1);head.next = new Node(2);head.next.next = new Node(3);head.next.next.next = new Node(2);head.next.next.next.next = new Node(1);printLinkedList(head);System.out.print(isPalindromeWithStack(head) + " | ");System.out.print(isPalindromeWithRightStack(head) + " | ");System.out.println(isPalindromeWithReverseList(head) + " | ");printLinkedList(head);System.out.println("=========================");head = new Node(1);head.next = new Node(2);head.next.next = new Node(3);head.next.next.next = new Node(3);head.next.next.next.next = new Node(2);head.next.next.next.next.next = new Node(1);printLinkedList(head);System.out.print(isPalindromeWithStack(head) + " | ");System.out.print(isPalindromeWithRightStack(head) + " | ");System.out.println(isPalindromeWithReverseList(head) + " | ");printLinkedList(head);System.out.println("=========================");}/*** 链表节点结构体*/public static class Node {private final int value;private Node next;public Node(int value) {this.value = value;}}public static void printLinkedList(Node head) {Node cur = head;int size = 0;while (cur != null) {size++;System.out.print(cur.value);if (cur.next != null) {System.out.print(" -> ");}cur = cur.next;}System.out.println(" 链表长度为:" + size);}
}
3、链表分区问题
链表的划分问题
- 将单向链表按某值划分成左边小、中间相等、右边大的形式
- 1)把链表放入数组里,在数组上做partition(笔试用)
- 2)分成小、中、大三部分,再把各个部分之间串起来(面试用)
3.1、用数组方式划分链表
/*** 数组方式划分链表* 将链表节点放到数组中,依据三分荷兰国旗问题进行划分,然后重新串起来* 时间复杂度O(N),额外空间复杂度O(N)*/public static Node linkedListPartitionWithArray(Node head, int pivot) {if (head == null || head.next == null) {return head;}// 先讲链表转为数组Node cur = head;int i = 0;while (cur != null) {i++;cur = cur.next;}Node[] nodeArr = new Node[i];i = 0;cur = head;for (i = 0; i != nodeArr.length; i++) {nodeArr[i] = cur;cur = cur.next;}/*** 利用荷兰国旗的思路,对数组进行三分,右侧大于区域的左边界为arr.length,左侧小于区域的右边界为-1* 遍历数组,根据荷兰国旗问题,将数组分为三部分*/int small = -1;int big = nodeArr.length;int index = 0;while (index < big) {if (nodeArr[index].value < pivot) {// 小于指定值,交换到小于区域的下一个位置,然后小于区域右移,当前值++swap(nodeArr, ++small, index++);} else if (nodeArr[index].value > pivot) {// 大于指定值,交换到大于区域的前一个位置,然后大于区域左移,当前值不变swap(nodeArr, --big, index);} else {// 等于指定值,当前值++index++;}}// 将数组串成链表for (i = 1; i != nodeArr.length; i++) {nodeArr[i - 1].next = nodeArr[i];}nodeArr[i - 1].next = null;return nodeArr[0];}public static void swap(Node[] nodeArr, int i, int j) {if (i == j) {return;}Node tmp = nodeArr[i];nodeArr[i] = nodeArr[j];nodeArr[j] = tmp;}
3.2、非数组方式划分链表
/*** 链表划分问题* 定义小于区域头和尾、等于区域头和尾、大于区域头和尾六个变量* 然后遍历链表,根据节点的值判断放到哪个区域里面,* 然后将三个区域串起来* 时间复杂度O(N),额外空间复杂度O(1)*/public static Node linkedListPartition(Node head, int pivot) {if (head == null || head.next == null) {return head;}// 定义小于区域头和尾、等于区域头和尾、大于区域头和尾六个变量Node smallHead = null;Node smallTail = null;Node equalHead = null;Node equalTail = null;Node biggerHead = null;Node biggerTail = null;// 遍历链表,根据节点的值判断放到哪个区域里面Node cur = head;// 循环时为了存放下一个记录,这样就可以将cur.next设置为null,防止后面引用乱掉Node next = null;while (cur != null) {// 先将cur.next赋值给next,防止后面引用乱掉next = cur.next;// 断开当前节点与后面节点的引用cur.next = null;if (cur.value < pivot) {// 小于指定值if (smallHead == null) {smallHead = cur;smallTail = cur;} else {smallTail.next = cur;smallTail = cur;}} else if (cur.value == pivot) {// 等于指定值if (equalHead == null) {equalHead = cur;equalTail = cur;} else {equalTail.next = cur;equalTail = cur;}} else {// 大于指定值if (biggerHead == null) {biggerHead = cur;biggerTail = cur;} else {biggerTail.next = cur;biggerTail = cur;}}cur = next;}// 连接三个区域if (smallHead != null) {smallTail.next = equalHead != null ? equalHead : smallHead;}if (equalTail != null) {equalTail.next = biggerHead;}return smallHead != null ? smallHead : (equalHead != null ? equalHead : biggerHead);}
完整代码:
/*** 链表的划分问题* 将单向链表按某值划分成左边小、中间相等、右边大的形式* 1)把链表放入数组里,在数组上做partition(笔试用)* 2)分成小、中、大三部分,再把各个部分之间串起来(面试用)*/
public class LinkedListPartition {/*** 数组方式划分链表* 将链表节点放到数组中,依据三分荷兰国旗问题进行划分,然后重新串起来* 时间复杂度O(N),额外空间复杂度O(N)*/public static Node linkedListPartitionWithArray(Node head, int pivot) {if (head == null || head.next == null) {return head;}// 先讲链表转为数组Node cur = head;int i = 0;while (cur != null) {i++;cur = cur.next;}Node[] nodeArr = new Node[i];i = 0;cur = head;for (i = 0; i != nodeArr.length; i++) {nodeArr[i] = cur;cur = cur.next;}/*** 利用荷兰国旗的思路,对数组进行三分,右侧大于区域的左边界为arr.length,左侧小于区域的右边界为-1* 遍历数组,根据荷兰国旗问题,将数组分为三部分*/int small = -1;int big = nodeArr.length;int index = 0;while (index < big) {if (nodeArr[index].value < pivot) {// 小于指定值,交换到小于区域的下一个位置,然后小于区域右移,当前值++swap(nodeArr, ++small, index++);} else if (nodeArr[index].value > pivot) {// 大于指定值,交换到大于区域的前一个位置,然后大于区域左移,当前值不变swap(nodeArr, --big, index);} else {// 等于指定值,当前值++index++;}}// 将数组串成链表for (i = 1; i != nodeArr.length; i++) {nodeArr[i - 1].next = nodeArr[i];}nodeArr[i - 1].next = null;return nodeArr[0];}public static void swap(Node[] nodeArr, int i, int j) {if (i == j) {return;}Node tmp = nodeArr[i];nodeArr[i] = nodeArr[j];nodeArr[j] = tmp;}/*** 链表划分问题* 定义小于区域头和尾、等于区域头和尾、大于区域头和尾六个变量* 然后遍历链表,根据节点的值判断放到哪个区域里面,* 然后将三个区域串起来* 时间复杂度O(N),额外空间复杂度O(1)*/public static Node linkedListPartition(Node head, int pivot) {if (head == null || head.next == null) {return head;}// 定义小于区域头和尾、等于区域头和尾、大于区域头和尾六个变量Node smallHead = null;Node smallTail = null;Node equalHead = null;Node equalTail = null;Node biggerHead = null;Node biggerTail = null;// 遍历链表,根据节点的值判断放到哪个区域里面Node cur = head;// 循环时为了存放下一个记录,这样就可以将cur.next设置为null,防止后面引用乱掉Node next = null;while (cur != null) {// 先将cur.next赋值给next,防止后面引用乱掉next = cur.next;// 断开当前节点与后面节点的引用cur.next = null;if (cur.value < pivot) {// 小于指定值if (smallHead == null) {smallHead = cur;smallTail = cur;} else {smallTail.next = cur;smallTail = cur;}} else if (cur.value == pivot) {// 等于指定值if (equalHead == null) {equalHead = cur;equalTail = cur;} else {equalTail.next = cur;equalTail = cur;}} else {// 大于指定值if (biggerHead == null) {biggerHead = cur;biggerTail = cur;} else {biggerTail.next = cur;biggerTail = cur;}}cur = next;}// 连接三个区域if (smallHead != null) {smallTail.next = equalHead != null ? equalHead : smallHead;}if (equalTail != null) {equalTail.next = biggerHead;}return smallHead != null ? smallHead : (equalHead != null ? equalHead : biggerHead);}public static void main(String[] args) {Node head1 = new Node(7);head1.next = new Node(9);head1.next.next = new Node(1);head1.next.next.next = new Node(8);head1.next.next.next.next = new Node(5);head1.next.next.next.next.next = new Node(2);head1.next.next.next.next.next.next = new Node(5);head1.next.next.next.next.next.next.next = new Node(4);head1.next.next.next.next.next.next.next.next = new Node(1);printLinkedList(head1);head1 = linkedListPartitionWithArray(head1, 4);printLinkedList(head1);head1 = linkedListPartition(head1, 5);printLinkedList(head1);}public static void printLinkedList(Node head) {Node cur = head;int size = 0;while (cur != null) {size++;System.out.print(cur.value);if (cur.next != null) {System.out.print(" -> ");}cur = cur.next;}System.out.println(" 链表长度为:" + size);}public static class Node {public int value;public Node next;public Node(int data) {this.value = data;}}
}
4、复制带随机指针的链表
复制带随机指针的链表
- 一种特殊的单链表节点类描述如下
- class Node {
- int value;
- Node next;
- Node rand;
- Node(int val) { value = val; }
- }
- rand指针是单链表节点结构中新增的指针,rand可能指向链表中的任意一个节点,也可能指向null。
- 给定一个由Node节点类型组成的无环单链表的头节点 head,请实现一个函数完成这个链表的复制,并返回复制的新链表的头节点。
- 【要求】
- 时间复杂度O(N),额外空间复杂度O(1)
- 测试链接 : https://leetcode.cn/problems/copy-list-with-random-pointer/description/
4.1、使用哈希表实现
/*** 方法一:使用哈希表实现* 先将所有节点放到一个哈希表中,key为老节点,value为新节点* 然后遍历链表,根据哈希表中的映射关系,设置新节点的next和random指针* 时间复杂度O(N),额外空间复杂度O(N)*/public static Node copyRandomListWithHead(Node head) {if (head == null) {return null;}// 先将所有节点放到一个哈希表中,key为老节点,value为新节点Map<Node, Node> map = new HashMap<Node, Node>();Node cur = head;while (cur != null) {map.put(cur, new Node(cur.val));cur = cur.next;}// 然后遍历链表,根据哈希表中的映射关系,设置新节点的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);}
4.2、不是用哈希表实现
/*** 方法二:不使用哈希表实现* 先遍历链表,复制每个节点,将新节点插入到旧节点的后面* 然后遍历链表,根据旧节点的random指针,设置新节点的random指针* 最后遍历链表,将新节点从旧节点的后面分离出来* 时间复杂度O(N),额外空间复杂度O(1)*/public static Node copyRandomList(Node head) {if (head == null) {return null;}// 先遍历链表,复制每个节点,将新节点插入到旧节点的后面// 例如原来是 1->2->3->null,变成1 -> 1' -> 2 -> 2' -> 3 -> 3'Node cur = head;Node next = null;while (cur != null) {// 先将下一个保存起来next = cur.next;// 复制一个新节点,作为当前节点的下一个cur.next = new Node(cur.val);// 复制节点的下一个指向保存的下一个cur.next.next = next;// 移动到下一个旧节点cur = next;}// 然后遍历链表,根据旧节点的random指针,设置新节点的random指针cur = head;while (cur != null) {// 新节点的random指针指向旧节点的random指针的下一个cur.next.random = cur.random == null ? null : cur.random.next;// 移动到下一个旧节点cur = cur.next.next;}// 最后遍历链表,将新节点从旧节点的后面分离出来cur = head;Node copyHead = head.next;Node copy = null;while (cur != null) {// 先保存好旧的nextnext = cur.next.next;// 复制的节点copy = cur.next;// 旧节点的下一个指向保存的旧的下一个cur.next = next;// 复制节点的下一个指向复制的下一个copy.next = next != null ? next.next : null;cur = cur.next;}return copyHead;}
5、找到两个单链表相交的第一个节点
找到两个单链表相交的第一个节点
- 给定两个可能有环也可能无环的单链表,头节点head1和head2。请实现一个函数,如果两个链表相交,请返回相交的 第一个节点。
- 如果不相交,返回null
- 这里的相交,指的是结构上的相交,不是值相同,即 node1 == node2
- 【要求】
- 如果两个链表长度之和为N,时间复杂度请达到O(N),额外空间复杂度 请达到O(1)。
思路: - 这个题最重要的一点是要先理清楚两个单向链表相交的情况。
- 1、因为是单向链表,如果是要要在某个点相交了,那必然后面的点都是共同拥有的了,不存在相交之后交叉的情况存在,因为如果交叉,那相交的点就有两个next指针了。
- 2、如果一个单线链表有环,那环一定是从开始点到结尾点的,也就是说结尾点一定在环上,不可能出去。
- 基于以上两点,就可以推出以下的结论:
- 1、如果两个链表一个有环,一个没有环,那它们一定不相交。
- 2、如果两个链表都没有环,其相交就是从相交点开始的后面节点都是共有的,要求出第一个相交的节点。就要看两个链表到相交点的距离了。
- 3、如果两个链表都有环,相交的点可能在环上,也可能不在环上。我们可以分别求出两个链表入环的第一个节点,就能有以下结论:
- 3.1、如果两个链表相交的点在环上,那连个链表入环的第一个节点均可以作为第一个入环的节点。
- 3.2、如果两个链表相交点不在环上,那就可以忽略环的位置,和两个五环链表相交的求法一样了。
求一个链表的入环的第一个节点:
- 定义一个快慢指针,如果两个不能相交,这证明链表无环。
- 如果相交,定义一个指针从相交点开始走,另一个从链表头结点开始走,再次相交的点,就是第一个相交的点。
- 过程:
- 1、求出两个链表第一个入环的点。
- 2、如果两个链表都是无环,则用无环链表的方式查找相交的点
- 3、如果两个链表都有环,则用两个链表都有环的方式找相交的点
完整实现和测试代码如下:
import java.util.HashSet;
import java.util.Set;/*** 找到两个单链表相交的第一个节点* 给定两个可能有环也可能无环的单链表,头节点head1和head2。请实现一个函数,如果两个链表相交,请返回相交的 第一个节点。* 如果不相交,返回null* 这里的相交,指的是结构上的相交,不是值相同,即 node1 == node2* 【要求】* 如果两个链表长度之和为N,时间复杂度请达到O(N),额外空间复杂度 请达到O(1)。*/
public class FirstIntersectNode {/*** 找到两个单链表相交的第一个节点* 思路:* 这个题最重要的一点是要先理清楚两个单向链表相交的情况。* 1、因为是单向链表,如果是要要在某个点相交了,那必然后面的点都是共同拥有的了,不存在相交之后交叉的情况存在,因为如果交叉,那相交的点就有两个next指针了。* 2、如果一个单线链表有环,那环一定是从开始点到结尾点的,也就是说结尾点一定在环上,不可能出去。* 基于以上两点,就可以推出以下的结论:* 1、如果两个链表一个有环,一个没有环,那它们一定不相交。* 2、如果两个链表都没有环,其相交就是从相交点开始的后面节点都是共有的,要求出第一个相交的节点。就要看两个链表到相交点的距离了。* 3、如果两个链表都有环,相交的点可能在环上,也可能不在环上。我们可以分别求出两个链表入环的第一个节点,就能有以下结论:* 3.1、如果两个链表相交的点在环上,那连个链表入环的第一个节点均可以作为第一个入环的节点。* 3.2、如果两个链表相交点不在环上,那就可以忽略环的位置,和两个五环链表相交的求法一样了。* 求一个链表的入环的第一个节点:* 定义一个快慢指针,如果两个不能相交,这证明链表无环。* 如果相交,定义一个指针从相交点开始走,另一个从链表头结点开始走,再次相交的点,就是第一个相交的点。* 过程:* 1、求出两个链表第一个入环的点。* 2、如果两个链表都是无环,则用无环链表的方式查找相交的点* 3、如果两个链表都有环,则用两个链表都有环的方式找相交的点**/public static Node getFirstIntersectNode(Node head1, Node head2) {if (head1 == null || head2 == null) {return null;}// 先找到两个链表第一个入环的点Node loopNode1 = getLoopNode(head1);Node loopNode2 = getLoopNode(head2);// 1、如果两个链表都无环,则用无环链表的方式查找相交的点if (loopNode1 == null && loopNode2 == null) {return getFirstIntersectNodeNoLoop(head1, head2);}// 2、如果两个链表都有环,则用两个链表都有环的方式找相交的点if (loopNode1 != null && loopNode2 != null) {return getFirstIntersectNodeLoop(head1, loopNode1, head2, loopNode2);}// 3、如果一个链表有环,一个链表无环,则一定不相交return null;}/*** 找到两个有环链表相交的第一个节点* 思路:* 两个有环的链表相交的话有两种情况,一种是相交点在环上,另一种是相交点不在环上。可以通过各自入环的第一个点来判断。* 如果两个链表入环的第一个点相同,那说明后面环上的点一定是共有的,第一次相交的有可能在入环节点前面,可以通过两个链表到相交点的差值,找出第一个相交的点。* 如果两个链表入环的第一个点不同,要么就是不相交,要么就是整个环都是共同的,我们可以从某一个入环节点开始循环一圈,如果找到和另一个相同的点,说明相交,返回任何一个相交的点即可。* 如果循环一圈没有找到和另一个入环节点相交的点,那么两个链表不相交*/private static Node getFirstIntersectNodeLoop(Node head1, Node loopNode1, Node head2, Node loopNode2) {if (head1 == null || head2 == null || loopNode1 == null || loopNode2 == null) {return null;}// 定义两个循环变量Node cur1 = null;Node cur2 = null;if (loopNode1 == loopNode2) {// 两个链表的第一个入环节点相同,则第一次相交的点有可能在之前,要用差值的办法查找cur1 = head1;cur2 = head2;int step = 0;while (cur1 != loopNode1) {step++;cur1 = cur1.next;}while (cur2 != loopNode2) {step--;cur2 = cur2.next;}// 找到长的链表赋值给cur1,短的给cur2cur1 = step > 0 ? head1 : head2;cur2 = cur1 == head1 ? head2 : head1;// 先让长的走差值步数step = Math.abs(step);while (step > 0) {cur1 = cur1.next;step--;}// 两个链表同时走,相遇的点就是第一个相交的点while (cur1 != cur2) {cur1 = cur1.next;cur2 = cur2.next;}return cur1;} else {// 两个入环节点不相同,要么不相交,要么整个环相交,// 我们可以从一个入环节点开始循环一圈,看能不能碰到另一个入环节点cur1 = loopNode1.next;while (cur1 != loopNode1) {if (cur1 == loopNode2) {// 找到和第二个入环节点相同的,说明相交,这里返回loopNode1和loopNode2都行return loopNode1;}cur1 = cur1.next;}// 循环一圈都没有找到,说明两个链表不相交return null;}}/*** 找到两个无环链表相交的第一个节点* 思路:* 如果两个无环的链表相交,那相交以后后面的点都是公共的点,要找到第一个相交的点,就要找出两个链表从头到相交点的距离。* 找到了这个距离,就可以让距离长的链表先走这个距离,然后两个链表再同时走,相遇的点就是第一个相交的点。* 过程:* 1、定义一个变量step,先循环第一个链表,每走一步step++,直到结尾。* 2、再循环第二个链表,每走一步step--,指导结尾。* 3、如果两次循环结尾的点不同,则证明没有相交,否则说明两个链表有相交* 4、这个时候的step,就是链表一和链表二长度差值,> 0表链表一长,< 0表链表二长,让长的先走step步数,然后两个一起走,第一个相同的点就是相交的点*/private static Node getFirstIntersectNodeNoLoop(Node head1, Node head2) {if (head1 == null || head2 == null) {return null;}// 分别循环两个链表,并求出步数差值int step = 0;Node cur1 = head1;while (cur1.next != null) {step++;cur1 = cur1.next;}Node cur2 = head2;while (cur2.next != null) {step--;cur2 = cur2.next;}// 这个时候cur1和cur2 都指向了两个链表的结尾节点// 如果结尾不相同,则代表不相交if (cur1 != cur2) {return null;}// 到这里,肯定相交,先让长的走step步数,这里可以用cur1和cur2来代替,为了方便阅读我重新定义了两个变量Node longList = step > 0 ? head1 : head2;Node shortList = longList == head1 ? head2 : head1;step = Math.abs(step);while (step > 0) {longList = longList.next;step--;}// 一起走,找到第一个相交的点while (longList != shortList) {longList = longList.next;shortList = shortList.next;}return longList;}/*** 找到链表第一个入环节点,如果无环,返回null* 1. 先判断链表是否有环,如果没有环,返回null* 2. 如果有环,判断环的入口节点* 快慢指针:slow慢指针,fast快指针,slow和fast同时从链表头出发,fast一次走两步,slow一次走一步,* 如果fast遇到null,说明链表无环,返回null;* 如果fast和slow相遇,说明链表有环,fast从链表头出发,slow从相遇点出发,每次都只走一步,* 再次相遇点就是环的入口节点*/private static Node getLoopNode(Node head) {if (head == null || head.next == null || head.next.next == null) {return null;}// 定义一个快慢指针,检测是否有环,如果有环,找到相交的点Node slow = head.next;Node fast = head.next.next;while (slow != fast) {if (fast.next == null || fast.next.next == null) {// 快指针都到结尾,说明没有环return null;}slow = slow.next;fast = fast.next.next;}// 走到这里,说明有环,让fast从头开始走,每次走一步,找到第一个相交的点fast = head;while (slow != fast) {slow = slow.next;fast = fast.next;}return slow;}public static void main(String[] args) {// 1->2->3->4->5->6->7->nullNode head1 = new Node(1);head1.next = new Node(2);head1.next.next = new Node(3);head1.next.next.next = new Node(4);head1.next.next.next.next = new Node(5);head1.next.next.next.next.next = new Node(6);head1.next.next.next.next.next.next = new Node(7);// 0->9->8->6->7->nullNode head2 = new Node(0);head2.next = new Node(9);head2.next.next = new Node(8);head2.next.next.next = head1.next.next.next.next.next; // 8->6printLinkedList(head1);printLinkedList(head2);System.out.println(getFirstIntersectNode(head1, head2).value);System.out.println("==============================================");// 1->2->3->4->5->6->7->4...head1 = new Node(1);head1.next = new Node(2);head1.next.next = new Node(3);head1.next.next.next = new Node(4);head1.next.next.next.next = new Node(5);head1.next.next.next.next.next = new Node(6);head1.next.next.next.next.next.next = new Node(7);head1.next.next.next.next.next.next = head1.next.next.next; // 7->4// 0->9->8->2...head2 = new Node(0);head2.next = new Node(9);head2.next.next = new Node(8);head2.next.next.next = head1.next; // 8->2printLinkedList(head1);printLinkedList(head2);System.out.println(getFirstIntersectNode(head1, head2).value);System.out.println("==============================================");// 0->9->8->6->4->5->6..head2 = new Node(0);head2.next = new Node(9);head2.next.next = new Node(8);head2.next.next.next = head1.next.next.next.next.next; // 8->6printLinkedList(head1);printLinkedList(head2);System.out.println(getFirstIntersectNode(head1, head2).value);System.out.println("==============================================");}public static void printLinkedList(Node head) {Node cur = head;int size = 0;// 存一下已经遍历的节点,Set<Node> set = new HashSet<>();while (cur != null) {if (set.contains(cur)) {System.out.print(" ===> " + cur.value);// 跳出循环break;} else {set.add(cur);}size++;System.out.print(cur.value);if (cur.next != null) {System.out.print(" -> ");}cur = cur.next;}System.out.println(" 链表长度为:" + size);}public static class Node {public int value;public Node next;public Node(int data) {this.value = data;}}
}
后记
个人总结,欢迎转载、评论、批评指正