LeetCode 08 - 分治
23. 合并K个升序链表#
方法:分治合并
每次两两合并,直到合并成两个有序链表,最后将这两个链表合并即可。
ListNode mergeKLists(ListNode[] lists) {
return merge(lists, 0, lists.length - 1);
}
ListNode merge(ListNode[] lists, int left, int right) {
if(left > right) return null;
if(left == right) return lists[left];
int mid = (left + right) / 2;
ListNode mergedLeft = merge(lists, left, mid);
ListNode mergedRight = merge(lists, mid+1, right);
return merge2Lists(mergedLeft, mergedRight);
}
ListNode merge2Lists(ListNode a, ListNode b) {
if(a == null || b == null)
return a == null ? b : a;
ListNode dummyHead = new ListNode(-1);
ListNode tail = dummyHead, pa = a, pb = b;
while(pa != null && pb != null) {
if(pa.val < pb.val) {
tail.next = pa;
pa = pa.next;
} else {
tail.next = pb;
pb = pb.next;
}
tail = tail.next;
}
tail.next = pa == null ? pb : pa;
return dummyHead.next;
}
169. 多数元素#
给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现数 大于 ⌊ n/2 ⌋
的元素。
方法一:哈希表
借助 HashMap
记录每个数字出现的次数,再遍历哈希表,找出值大于 n/2
的键。
方法二:排序
对数组排序(递增递减皆可)Arrays.sort(nums)
,那么下标在 n/2
的元素一定是答案。
方法三:随机化
因为多数元素占据了一半以上,所以可以随机挑选一个元素,检查它是不是众数,这样有很大概率找到答案。
int countOccurences(int[] nums, int num) {
int count = 0;
for(int i = 0; i < nums.length; i++)
if(nums[i] == num) count++;
return count;
}
int majorityElement(int[] nums) {
int len = nums.length;
Random rand = new Random();
int majorityCount = len/2;
while(true) {
int randNum = rand.nextInt(len);
if(countOccurences(nums, randNum) > majorityCount)
return randNum;
}
}
方法四:分治
如果 a
是数组 nums
的众数,那么将数组分成两个子数组,a
仍然是其中至少一个子数组的众数。
这样一来就可以用分治法了:将数组分成左右两部分,分别求出左半部分的众数 a1
以及右半部分众数 a2
,然后在这两个众数中选出答案。
int countInRange(int[] nums, int target, int low, int high) {
int count = 0;
for(int i = low; i <= high; i++)
if(nums[i] == target) count++;
return count;
}
int recurse(int[] nums, int low, int high) {
// base case
if(low == high) return nums[low];
// 递归
int mid = (low + high) / 2;
int left = recurse(nums, low, mid);
int right = recurse(nums, mid + 1, high);
// 如果两个众数相同直接返回
if(left == right) return left;
// 不相同则进一步比较
int leftCount = countInRange(nums, left, low, high);
int rightCount = countInRange(nums, right, low, high);
return leftCount > rightCount ? left : right;
}
int majorElement(int nums) {
return recurse(nums, 0, nums.length - 1);
}
方法五:Boyer-Moore 投票算法
基本思想是,把 majority 元素记为 +1,把其他元素记为 -1,则所有元素的和会大于 0(因为题目保证一定存在 majority 元素)。具体算法过程为:
- 维护一个候选元素
candidate
及其出现次数count
,初始时候选元素任选,次数为 0。 - 遍历数组,在处理每个元素
x
之前,如果count==0
,则将x
作为candidate
,这时候,- 如果
x
和原来的candidate
相同,则count++
(变成 1)。 - 否则,
count--
(变成 -1)。
- 如果
- 遍历结束后,
candidate
即为整个数组的 majority 元素。
为什么这么做是对的呢?以 candidate
变成 0
的位置为界,前半部分数组中,majority 元素的个数 <=
其他数的总数,所以后半部分数组中,majority 元素的个数一定 >
其他所有数的总数,也就是说 majority 元素也是后半部分子数组的 majority 元素。总的来说, candidate=0
将数组分成若干段,每个 candidate=0
右边的子数组的 majority 元素一定是整个数组的 majority 元素。
int majority(int[] nums) {
int candidate = -1, count = 0;
for(int num : nums) {
// 在分界点换人
if(count == 0) candidate = num;
if(num == candidate) count++;
else count--;
}
return candidate;
}
240. 搜索二维矩阵 II#
编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性:
- 每行的元素从左到右升序排列。
- 每列的元素从上到下升序排列。
方法一:二分搜索
在每一行中寻找目标值时,利用二分搜索。
boolean searchMatrix(int[][] matrix, int target) {
for(int[] row : matrix) {
if(search(row, target))
return true;
}
return false;
}
boolean search(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while(left <= right) {
int mid = (left + right) / 2;
int midValue = nums[mid];
if(midValue == target)
return true;
else if(midValue < target)
left = mid + 1;
else
right = mid - 1;
}
return false;
}
方法二:Z字形搜索
从矩阵右上角开始搜索,在每一步搜索过程中,如果当前位置为 ,那么搜索范围确定为:以矩阵左下角为左下角,以当前位置为右上角的矩阵范围。
- 如果
matrix[x][y] = target
,说明搜索完成; - 如果
matrix[x][y] > target
,说明当前列 中所有 行之下的元素都大于目标值,所以可以直接将y--
; - 如果
matrix[x][y] < target
,说明当前行 中所有 列之前的元素都小于目标值,可以直接将x++
。 - 如果超出矩阵边界,说明不存在 target。
boolean searchMatrix(int[][] matrix, int target) {
int rows = matrix.length, cols = matrix[0].length;
int row = 0, col = cols-1;
while(row < rows && col >= 0) {
if(matrix[row][col] == target)
return true;
else if(matrix[row][col] < target)
row++;
else
col--;
}
return false;
}
148. 排序链表#
将给定链表升序排序,并返回排序后的链表。
最适合链表的排序算法是归并排序,归并排序基于分治算法,最容易想到的实现方式是自顶向下的递归实现,但是递归调用的栈空间为 ,所以要达到常数的空间复杂度,需要使用自底向上的方式。
方法一:自顶向下归并排序
过程如下:
- 找到链表的中点,将链表拆分为两个子链表;(快慢指针)
- 对两个子链表分别排序;
- 将排序后的子链表进行合并。
ListNode sortList(ListNode head) {
return sortListHelper(head, null);
}
ListNode sortListHelper(ListNode head, ListNode tail) {
// 递归终止条件:只剩一个/0个结点
if(head == null) return null;
if(head.next == tail) {
head.next = null;
return head;
}
// 找到中点
ListNode slow = head, fast = head;
while(fast != tail) {
slow = slow.next;
fast = fast.next;
if(fast != tail)
fast = fast.next;
}
ListNode mid = slow;
// 递归排序两个子链表
ListNode list1 = sortListHelper(head, mid);
ListNode list2 = sortListHelper(mid, tail);
// 将子链表合并成一个有序链表
ListNode sorted = merge(list1, list2);
return sorted;
}
// 合并两个链表,变成一个有序链表
public ListNode merge(ListNode head1, ListNode head2) {
ListNode dummyHead = new ListNode(0);
ListNode temp = dummyHead, temp1 = head1, temp2 = head2;
while(temp1 != null && temp2 != null) {
if(temp1.val <= temp2.val) {
temp.next = temp1;
temp1 = temp1.next;
} else {
temp.next = temp2;
temp2 = temp2.next;
}
temp = temp.next;
}
if(temp1 != null) temp.next = temp1;
else if (temp2 != null) temp.next = temp2;
return dummyHead.next;
}
方法二:自底向上归并排序
过程如下:
- 用
subLength
表示每次需要排序的子链表的长度,初始时subLength=1
。 - 每次将链表拆分成若干个长度为
subLength
的子链表,按照每两个子链表一组进行合并,合并后即可得到若干个长度为subLength * 2
的有序子链表。 - 将
subLength
加倍,重复第 2 步,对更长的有序子链表进行合并,直到有序子链表的长度达到length
。
ListNode sortList(ListNode head) {
if(head == null) return null;
// 求出链表长度
int length = 0;
ListNode node = head;
while(node != null) {
length++;
node = node.next;
}
ListNode dummyHead = new ListNode(0, head);
// 每次将链表拆分为若干个长度为 subLength 的链表,并两两一组合并
for(int subLength = 1; subLength < length; subLength <<= 1) {
// cur 用于记录拆分位置, prev 用来串联合并后的子链表
ListNode prev = dummyHead, cur = dummyHead.next;
while(cur != null) {
// 拆出来 subLength 长度的链表1
ListNode head1 = cur;
for(int i = 1; i < subLength && cur != null && cur.next != null; i++)
cur = cur.next;
// 拆出来 subLength 长度的链表2
ListNode head2 = cur.next; // 链表2的头就是链表1尾结点的下一个节点
cur.next = null; // 断开链表1和2的连接
cur = head2;
// 找到链表2的结束位置
for(int i = 1; i < subLength && cur != null && cur.next != null; i++)
cur = cur.next;
ListNode next = null; // next 缓存下一个子链表的开始位置
if(cur != null) {
next = cur.next;
cur.next = null; // 断开链表2和后面的连接
}
// 合并两个子链表的
ListNode merged = merge(head1, head2);
// 将合并后的子链表串联到总链表
prev.next = merged;
// 更新 prev 和 cur 指针
while(prev.next != null)
prev = prev.next;
cur = next;
}
}
}
两个方法的时间都是:。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义