深入浅出Java算法 排序与搜索
排序和搜索是算法中最基础也最常考的两大类问题,今天我就用最通俗的大白话,给大家拆解这些高频考题的解题套路。
一、排序算法三板斧
1. 快速排序(快排)
核心思想:挑个基准值,小的放左边,大的放右边,递归处理两边
java
public void quickSort(int[] arr, int low, int high) {
if (low < high) {
int pivot = partition(arr, low, high); // 找分割点
quickSort(arr, low, pivot - 1); // 排左边
quickSort(arr, pivot + 1, high); // 排右边
}
}
private int partition(int[] arr, int low, int high) {
int pivot = arr[high]; // 选最后一个当基准
int i = low; // 小于基准的边界
for (int j = low; j < high; j++) {
if (arr[j] < pivot) {
swap(arr, i, j); // 小的扔到左边
i++;
}
}
swap(arr, i, high); // 基准归位
return i;
}
解释:
- 随便抓个人当基准(比如最后一个)
- 比他矮的站左边,高的站右边
- 让基准站到中间
- 对左右两队重复这个过程
2. 归并排序
核心思想:先拆到最小,再两两合并
java
public void mergeSort(int[] arr, int left, int right) {
if (left < right) {
int mid = (left + right) / 2;
mergeSort(arr, left, mid); // 排左边
mergeSort(arr, mid + 1, right); // 排右边
merge(arr, left, mid, right); // 合并
}
}
private void merge(int[] arr, int left, int mid, int right) {
// 临时数组存合并结果
int[] temp = new int[right - left + 1];
int i = left, j = mid + 1, k = 0;
// 两两比较,小的先放
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
temp[k++] = arr[i++];
} else {
temp[k++] = arr[j++];
}
}
// 处理剩下的
while (i <= mid) temp[k++] = arr[i++];
while (j <= right) temp[k++] = arr[j++];
// 拷贝回原数组
System.arraycopy(temp, 0, arr, left, temp.length);
}
解释:
- 把数组对半切
- 切到只剩一个元素时开始合并
- 合并时像军训排队,两个排好序的队伍,谁小谁先出列
3. 堆排序
核心思想:先把数组堆化,然后逐个取出最大值
java
public void heapSort(int[] arr) {
// 1. 构建大顶堆
for (int i = arr.length/2 - 1; i >= 0; i--) {
heapify(arr, arr.length, i);
}
// 2. 逐个提取元素
for (int i = arr.length - 1; i > 0; i--) {
swap(arr, 0, i); // 把当前最大的放到最后
heapify(arr, i, 0); // 调整剩下的堆
}
}
private void heapify(int[] arr, int n, int i) {
int largest = i; // 父节点
int left = 2 * i + 1;
int right = 2 * i + 2;
if (left < n && arr[left] > arr[largest]) {
largest = left;
}
if (right < n && arr[right] > arr[largest]) {
largest = right;
}
if (largest != i) {
swap(arr, i, largest);
heapify(arr, n, largest); // 递归调整
}
}
解释:
- 先把数组变成大顶堆(父节点比子节点大)
- 堆顶是最大的,把它和最后一个元素交换
- 剩下的元素重新堆化
- 重复直到全部有序
二、搜索算法两大利器
1. 二分查找
适用场景:有序数组中找特定值
java
public int binarySearch(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1; // 去右边找
} else {
right = mid - 1; // 去左边找
}
}
return -1; // 没找到
}
变种题型:
- 找第一个等于target的
- 找最后一个等于target的
- 找第一个大于等于target的
java
// 找第一个等于target的
public int firstEqual(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] >= target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
if (left < nums.length && nums[left] == target) {
return left;
}
return -1;
}
解释:
- 每次猜中间那个数
- 如果猜大了就往左找,猜小了就往右找
- 直到找到或者确定不存在
2. 深度优先搜索(DFS)和广度优先搜索(BFS)
DFS模板(递归版):
java
void dfs(TreeNode root) {
if (root == null) return;
// 前序遍历
System.out.println(root.val);
dfs(root.left);
dfs(root.right);
// 中序和后序调整打印位置即可
}
BFS模板:
java
void bfs(TreeNode root) {
if (root == null) return;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
TreeNode node = queue.poll();
System.out.println(node.val);
if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
}
}
解释:
- DFS:一条路走到黑,撞墙再回头
- BFS:一层一层扫荡,像水波纹扩散
三、高频考题解题套路
1. 合并两个有序数组
问题:把nums2合并到nums1中(nums1有足够空间)
java
public void merge(int[] nums1, int m, int[] nums2, int n) {
int p1 = m - 1, p2 = n - 1, p = m + n - 1;
while (p1 >= 0 && p2 >= 0) {
nums1[p--] = nums1[p1] > nums2[p2] ? nums1[p1--] : nums2[p2--];
}
// 如果nums2还有剩余
System.arraycopy(nums2, 0, nums1, 0, p2 + 1);
}
解释:从后往前放,谁大谁先占坑
2. 寻找两个正序数组的中位数
解题思路:转化成找第k小的数
java
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m = nums1.length, n = nums2.length;
int left = (m + n + 1) / 2;
int right = (m + n + 2) / 2;
return (findKth(nums1, 0, nums2, 0, left) + findKth(nums1, 0, nums2, 0, right)) / 2.0;
}
private int findKth(int[] nums1, int i, int[] nums2, int j, int k) {
if (i >= nums1.length) return nums2[j + k - 1];
if (j >= nums2.length) return nums1[i + k - 1];
if (k == 1) return Math.min(nums1[i], nums2[j]);
int midVal1 = (i + k/2 - 1 < nums1.length) ? nums1[i + k/2 - 1] : Integer.MAX_VALUE;
int midVal2 = (j + k/2 - 1 < nums2.length) ? nums2[j + k/2 - 1] : Integer.MAX_VALUE;
if (midVal1 < midVal2) {
return findKth(nums1, i + k/2, nums2, j, k - k/2);
} else {
return findKth(nums1, i, nums2, j + k/2, k - k/2);
}
}
解释:每次排除掉k/2个不可能的数,逐步逼近中位数
3. 前K个高频元素
解题思路:哈希表统计频率 + 最小堆
java
public int[] topKFrequent(int[] nums, int k) {
// 统计频率
Map<Integer, Integer> freqMap = new HashMap<>();
for (int num : nums) {
freqMap.put(num, freqMap.getOrDefault(num, 0) + 1);
}
// 最小堆保存前k大
PriorityQueue<Map.Entry<Integer, Integer>> heap =
new PriorityQueue<>((a, b) -> a.getValue() - b.getValue());
for (Map.Entry<Integer, Integer> entry : freqMap.entrySet()) {
heap.offer(entry);
if (heap.size() > k) {
heap.poll();
}
}
// 收集结果
int[] res = new int[k];
for (int i = k - 1; i >= 0; i--) {
res[i] = heap.poll().getKey();
}
return res;
}
解释:
- 先数每个数字出现的次数
- 用一个小顶堆来保存出现次数最多的k个
- 堆满了就把最小的踢出去
四、实战技巧
-
排序选择:
- 数据量小用插入排序
- 需要稳定用归并
- 一般情况用快排
- 找前K大/小用堆排序
-
二分查找变种:
- 找边界时,先确定搜索方向
- 结束时检查left/right是否越界
- 不确定时画图模拟
-
搜索优化:
- DFS考虑剪枝
- BFS考虑双向BFS
- 矩阵搜索可以DFS+记忆化
-
必刷题目:
- LeetCode 215:数组中的第K个最大元素
- LeetCode 347:前K个高频元素
- LeetCode 4:寻找两个正序数组的中位数
- LeetCode 33:搜索旋转排序数组
- LeetCode 200:岛屿数量
记住这些套路,排序搜索题就能见招拆招。最重要的是多写多练,培养对算法复杂度的直觉!