【day11】技巧+链表
1、下一个排列 hot
题目:31. 下一个排列 - 力扣(LeetCode)

分析:就是找到下一个更大的数字。以 [1,3,5,4,2] 为例。
- 第一步:从右向左,找到第一个数 x 小于其右邻数,作为较小数,示例是 3。为什么:因为我们想找一个较小数与其右边的较大数交换(让序列变大),那么 x 右边必须要有比它大的;交换后较大数的位置尽量靠右(让序列变大的幅度尽量小),那么就从右向左找第一个满足其右有较大数的较小数;因为是第一个其右有较大数的较小数,因此其右必是降序排列(其右的每个数的右边都不存在比每个数大的),那么我们只需要从右向左,用x的右邻数是否比x大,来判断 x 是否是第一个其右存在比它大的数的数(x 必是第一个破坏降序的数)。
- 第二步:从右向左,找到第一个数 y 大于较小数 x,作为较大数,示例是 4。为什么:交换到前面的较大数应尽量小(让序列变大的幅度尽量小)。
- 第三步:交换 x 与 y,最后翻转 y 后的子数组,示例结果 [1,4,2,3,5]。为什么:交换后,y 后的子数组必然保持降序(y 是最小的大于 x 的数,原位 y 其右的数必然比 x 小,原位 y 其左的数必然比 x 大),翻转后子数组为升序(让序列变大的幅度尽量小)。
- 特殊情况考虑:降序数组是字典中最大的,只需要执行第三步翻转,就能回到最小的升序数组。
时间复杂度:找较小数x O(n),找较大数 y O(n),翻转 O(n),总体 O(n)。空间复杂度 O(1)。
代码:
class Solution {public void nextPermutation(int[] nums) {// 1. 从右到左,找到第一个较小数 x:小于右邻数int i = nums.length-2;int x;while (i >= 0 && nums[i] >= nums[i+1]) i--; // 降序的情况,找不到 x,最后 i=-1if (i >= 0) x = nums[i]; // 降序没有 x i>=0// 2. 从右到左,找到第一个较大数 yint j = nums.length-1;int y;while(i >= 0 && j > i && nums[j] <= nums[i]) j--; // 降序没有 y i>=0if (i >= 0) {y = nums[j];swanp(nums, i, j); // 交换 x 和 y}// 3. 翻转 y 后的子数组 [i+1, n-1];若降序数组 i=-1,翻转全部 [0=i+1, n-1]reverse(nums, i+1, nums.length-1);}private void swanp(int[] nums, int i, int j) {int t = nums[i];nums[i] = nums[j];nums[j] = t;}private void reverse(int[] nums, int i, int j) {while (i < j) swanp(nums, i++, j--);}
}
2、多数元素 hot
题目:169. 多数元素 - 力扣(LeetCode)

分析:
哈希表:哈希表统计每种数字的个数O(n),然后再遍历哈希表找到个数最大的元素(题目说必定存在多数元素)O(n)。时间复杂度 O(n),空间复杂度 额外哈希表 O(n)。
投票法:
- 原则:多数元素(+1)的个数一定比其他元素(-1)总数多,那么最后计分必>0。
- 每次假设剩下元素的首元素为多数元素,直到计数抵消为 0:
若假设正确,那么被消去的数中有一半是众数。
若假设错误,那么被消去的数中有 0 ~ 一半是多数元素(更多非众数元素被消掉,众数比例更多)。
这两种假设都不会打破原则。
- 最终结果:剩余数组无法再抵消为0,计数大于0。若首元素不是众数,那么首元素还能被众数抵消为 0,所以最终剩余数组的首元素必然是众数。
时间复杂度:O(n),空间复杂度 O(1)。
代码:
哈希表:
class Solution {public int majorityElement(int[] nums) {// 统计每种元素的个数Map<Integer, Integer> counts = new HashMap<>();for (int num : nums) counts.put(num, counts.getOrDefault(num, 0)+1);// 遍历哈希表,找到计数中最大的数Map.Entry<Integer, Integer> max = null;for (Map.Entry<Integer, Integer> count : counts.entrySet()) {if (max == null || max.getValue() < count.getValue())max = count;}return max.getKey(); }
}
投票:
class Solution {public int majorityElement(int[] nums) {// 1. 假设首元素是众数,开始计数,众数计1,非众数计-1// 2. 抵消为 0 后执行 1.int cnt = 0, n = 0;for (int i = 0; i < nums.length; i++) {if (cnt == 0) n = nums[i];if (nums[i] == n) cnt++;else cnt--;}// 最终的首元素就是多数元素return n;}
}
3、合并两个有序链表 hot
题目:21. 合并两个有序链表 - 力扣(LeetCode)

分析:类似归并排序中,合并步骤。创建一个头指针,每次比较 i、j 节点哪个更小,小的尾插入链表。最后把剩下的一条全部入链表。
代码:
/*** 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 mergeTwoLists(ListNode list1, ListNode list2) {ListNode head = new ListNode();ListNode k = head;while (list1 != null && list2 != null) {if (list1.val <= list2.val) {k.next = list1;list1 = list1.next;} else {k.next = list2;list2 = list2.next;}k = k.next;}if (list1 != null) k.next = list1;if (list2 != null) k.next = list2;return head.next;}
}
4、排序链表 hot
题目:148. 排序链表 - 力扣(LeetCode)

分析:
法一,自顶向下归并排序(递归的方式):时间复杂度 O(nlogn),空间复杂度 递归深度 O(logn)。
法二,自底向上归并排序(迭代的方式):step 从 1 增加到 < arr.length,每次循环 step 翻倍。如 arr=[4, 2, 1, 3]。
① 第一次外循环,step=1。第一次内循环,分割出 4 和 2 排序 2,4;第二次内循环,分割出 1 和 3 排序 1,3。
② 第二次外循环,step=2*step=2。第一次内循环,分割出 2,4 和 1,3 排序 1,2,3,4。
③ step = 2*step = 4 >= arr.length,停止排序。
时间复杂度:计算长度O(n),每轮 step,划分数组 O(n),合并数组 O(n)。一共 O(logn) 个不同 step,综合O(n+2nlogn)=O(nlogn);空间复杂度:没有递归 O(1)。
代码:
自顶向下归并排序:
/*** 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 {// 快慢指针,快指针走到了结尾时,慢指针指向终点private ListNode getMiddleNode(ListNode head) {if (head == null || head.next == null) return head;ListNode slow = head, fast = head.next;while (fast != null && fast.next != null) {slow = slow.next;fast = fast.next.next;}ListNode pre = slow; // 中点slow = slow.next; // list2 链头pre.next = null; // 断开连接 return slow; // 返回list2链头}public ListNode sortList(ListNode head) {if (head == null || head.next == null) return head; // 不需要排序了// 划分,左右排序ListNode list2 = getMiddleNode(head);ListNode list1 = sortList(head);list2 = sortList(list2);// 合并return merge(list1, list2);}private ListNode merge(ListNode list1, ListNode list2) {ListNode head = new ListNode();ListNode k = head;while (list1 != null && list2 != null) {if (list1.val <= list2.val) {k.next = list1;list1 = list1.next;} else {k.next = list2;list2 = list2.next;}k = k.next;}if (list1 != null) k.next = list1;if (list2 != null) k.next = list2;return head.next;}
}
自底向上归并排序:
/*** 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 {// 2. 自底向上public ListNode sortList(ListNode head) {// 求链表长度int length = getLength(head);// 外循环,控制 stepListNode newHead = head; // 指向每一次新链表的链头for (int step = 1; step < length; step *= 2) {ListNode tmp = newHead; // 指向下一个划分的链头ListNode newTail = null; // 新链表链尾// 内循环,对于当前 step 划分,控制是否继续划分并排序(下一个划分头为 null 表示没有可排序的子数组)while (tmp != null) {// 划分出 list1 和 list2ListNode list1 = tmp;ListNode list2 = getStepList(list1, step);tmp = getStepList(list2, step);// 合并左右子数组,排序。返回新链表头、尾ListNode[] newHeadTail = merge(list1, list2);if (newTail == null) { // 新链表的第一个部分,更新头和尾newHead = newHeadTail[0];newTail = newHeadTail[1];}else { // 新链表的后续部分,头不需要更新,尾连接下一个合并后的头,然后更新尾newTail.next = newHeadTail[0];newTail = newHeadTail[1];}}}return newHead;}// 划分出一个子数组后,返回下一个划分头 private ListNode getStepList(ListNode head, int step) {// 如果是空数组,直接返回 nullif (head == null) return null;// 获取子数组最后一个节点;也可能没有下一个划分了,由第二个条件退出,返回 nullwhile (step != 1 && head.next != null) {head = head.next;step--;}// 当前划分尾指向 null,返回下一个划分头ListNode ret = head.next;head.next = null;return ret;}private ListNode[] merge(ListNode list1, ListNode list2) {ListNode newHead = new ListNode();ListNode newTail = newHead;while (list1 != null && list2 != null) {if (list1.val <= list2.val) {newTail.next = list1;list1 = list1.next;} else {newTail.next = list2;list2 = list2.next;}newTail = newTail.next;}if (list1 != null) newTail.next = list1;if (list2 != null) newTail.next = list2;// 让尾指针走到合并后的链表尾部while (newTail.next != null) newTail = newTail.next;return new ListNode[]{newHead.next, newTail};}private int getLength(ListNode head) {int count = 0;while (head != null) {count++;head = head.next;}return count;}
}
其实实际的执行效率,时间上法一更短,空间上法二更少,这就取决于更需要时间还是空间了。
