2025高频面试算法总结篇【排序】
文章目录
- 直接刷题链接直达
- 把数组排成最小的数
- 删除有序数组中的重复项
- 求两个排序数组的中位数
- 求一个循环递增数组的最小值
- 数组中的逆序对
- 如何找到一个无序数组的中位数
- 链表排序
- 从一大段文本中找出TOP K 的高频词汇
直接刷题链接直达
- 把一个数组排成最大的数
- 剑指 Offer 45. 把数组排成最小的数
- 面试官说需要 通过 补位 思想(普通的compare再sort并不满意,但补位思想似乎不适用于有重复元素的情况)
- 如何给一个很大的无序数组去重
- 26. 删除排序数组中的重复项(给排序数组去重)
- 求两个排序数组的中位数
- 要求时间复杂度 O(log(m+n))
- 二分查找,递归求整个数组中第K大的元素,完整代码需要仔细考虑多种边界条件
- 4.寻找两个有序数组的中位数
- 求一个循环递增数组的最小值
- 153. 寻找旋转排序数组中的最小值 (无重复元素,二分,总有一半有序,注意边界)
- 154. 寻找旋转排序数组中的最小值 II (有重复元素,需要解除相等时的死循环)
- 数组中的逆序对
- 归并排序 && 递归的应用
- 引入辅助数组临时存放排序好的数据
- 归并时指向两个指针末尾,逐次向前并统计
- 面试题51. 数组中的逆序对
- 如何找到一个无序数组的中位数
- 295. 数据流的中位数 (建立两个堆,最大堆&最小堆,复杂度分析)
- 找出一个无序数组的中位数 (快排,缩小Partition区域 / 取一半元素建堆)
- 链表排序
- 需要 nlog(n) 时间复杂度和常数级空间复杂度
- 归并排序的应用(Bottom Up)
- 找到中点,断开链表(通过快慢两个指针)
- 交替双指针合并
- 148. 排序链表
- 手写主流排序算法 & 各种算法的复杂度/稳定性分析
- 常见问题
- 手写快排 / 堆排
- 快排的复杂度分析(最好/最坏/平均)
- 堆排中建堆的时间复杂度分析 --> O(n)
- 堆排序中建堆过程时间复杂度O(n)怎么来的?
- 为什么建堆的时间复杂度是O(n)?
- 归并排序的 Top-Down & Bottom-up 策略
- 不同排序的稳定性分析
- 冒泡排序的优化策略(华为)
- 设置flag位,一轮未交换数据即已完成排序,提前结束
- 记住本轮最后一次交换发生的位置lastExchange,下次内层循环到此终止即可
- 排序算法稳定性
- 排序算法可视化
- 快排 Wiki / 堆排 Wiki / 归并排序 Wiki
- 堆排序(Heapsort) (特别好的讲解)
- 冒泡排序算法及其两种优化
- 常见问题
- (Top K 问题)给定一个无符号,包含10亿个数的数组,如何取出前100大的数
- 答题思路
- 首先询问资源 --> 内存 / 核数 / 单机or多机,如可用多机 --> MapReduce思想
- 堆排 O(nlogk),可以单机处理海量数据(在内存受限情况下),如果k较小,趋近于 O(n)
- 建立一个容量为k的大/小顶堆
- n个元素逐一比较,O(logk) 完成删除和插入操作
- 全局排序, O(nlogn) (数据量较小时才可行)
- 冒泡(k个),O(kn)
- 快排划分 O(n), 每次递归处理一侧的数据,理论上可以理解为每次折半,缺点 --> 存在内存不够的问题,因为需要一次读入所有数据
- 算法必学:经典的 Top K 问题(基本思路篇)
- 海量数据处理 - 10亿个数中找出最大的10000个数(top K问题) (各种资源场景分析,面试前可参考)
- 最小的K个数(代码实现,首选堆排)
- 答题思路
- Java自带的 sort() 方法是如何实现的
- Array.sort() / Collections.sort()
- DualPivotQuicksort(双轴快速排序)
- Arrays.sort和Collections.sort实现原理解析
- Collections.sort()和Arrays.sort()排序算法选择
- 写一个快速划分数据集的算法,要求测试集用新数据,训练集用老数据
- 数据格式为 Record(id, timestamp)
- 函数签名为 division(ArrayList dataset, double ratio), ratio为(0,1)的划分比例
- 要求复杂度为O(n)
- 从一大段文本中找出TOP K 的高频词汇
- System Design Interview - Top K Problem (Heavy Hitters) (系统设计角度思考本题,如何权衡性能和效率,较为高阶)
- 347. 前K 个高频元素 (数字频次代码实现,建堆,时间复杂度为nlog(k))
- 692. 前K个高频单词 (词汇频次代码实现,思路一致)
把数组排成最小的数
题目:闯关游戏需要破解一组密码,闯关组给出的有关密码的线索是:
- 一个拥有密码所有元素的非负整数数组 password
- 密码是 password 中所有元素拼接后得到的最小的一个数
class Solution {
public String crackPassword(int[] password) {
String[] strs = new String[password.length];
for (int i=0; i < password.length; i++) {
strs[i] = password[i] + "";
}
Arrays.sort(strs, (a, b) -> (a + b).compareTo(b + a));
StringBuilder sb = new StringBuilder();
for (int i=0; i < password.length; i++) {
sb.append(strs[i]);
}
return sb.isEmpty() ? "0":sb.toString();
}
}
删除有序数组中的重复项
给你一个 非严格递增排列 的数组 nums
,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums
中唯一元素的个数。
考虑 nums
的唯一元素的数量为 k ,你需要做以下事情确保你的题解可以被通过:
- 更改数组 nums ,使 nums 的前 k 个元素包含唯一元素,并按照它们最初在 nums 中出现的顺序排列。nums 的其余元素与 nums 的大小不重要。
- 返回 k 。
class Solution {
public int removeDuplicates(int[] nums) {
if (nums.length == 0) return 0;
int k = 0;
for (int i = 1; i < nums.length; i++) {
if (nums[i] != nums[k]) {
nums[++k] = nums[i];
}
}
return k+1;
}
}
求两个排序数组的中位数
题目:
给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。
- 算法的时间复杂度应该为 O(log (m+n)) 。
解法:
这道题的关键是 二分查找+数组切割,核心思路是 在较短数组上二分查找,然后通过数学推导找到合适的中位数。
-
设定两个数组
nums1
和nums2
,保证nums1
总是最短的数组(这样可以减少二分查找的搜索范围)。 -
对短数组进行二分查找,设
nums1
的长度为m
,nums2
的长度为n
,则我们希望找到一个分割点i
(在nums1
中),同时j = (m + n + 1) / 2 - i
(在nums2
中)。 -
确保分割点左侧的所有元素 ≤ 右侧的所有元素:
-
nums1[i-1] <= nums2[j]
-
nums2[j-1] <= nums1[i]
-
确定中位数:
-
如果
m + n
是 奇数,中位数是左半部分的最大值max(nums1[i-1], nums2[j-1])
。 -
如果
m + n
是 偶数,中位数是(max(nums1[i-1], nums2[j-1]) + min(nums1[i], nums2[j])) / 2
。
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
// 保证 nums1 是较短的数组
if (nums1.length > nums2.length) {
return findMedianSortedArrays(nums2, nums1);
}
int m = nums1.length, n = nums2.length;
int left = 0, right = m;
int medianPos = (m + n + 1) / 2; // 中位数的位置
while (left <= right) {
int i = left + (right - left) / 2; // nums1的分割点
int j = medianPos - i; // nums2的分割点
int nums1LeftMax = (i == 0) ? Integer.MIN_VALUE : nums1[i - 1];
int nums1RightMin = (i == m) ? Integer.MAX_VALUE : nums1[i];
int nums2LeftMax = (j == 0) ? Integer.MIN_VALUE : nums2[j - 1];
int nums2RightMin = (j == n) ? Integer.MAX_VALUE : nums2[j];
if (nums1LeftMax <= nums2RightMin && nums2LeftMax <= nums1RightMin) {
// 找到合适的分割点
if ((m + n) % 2 == 0) {
return (Math.max(nums1LeftMax, nums2LeftMax) + Math.min(nums1RightMin, nums2RightMin)) / 2.0;
} else {
return Math.max(nums1LeftMax, nums2LeftMax);
}
} else if (nums1LeftMax > nums2RightMin) {
// 需要向左移动
right = i - 1;
} else {
// 需要向右移动
left = i + 1;
}
}
throw new IllegalArgumentException("输入的数组不符合条件");
}
求一个循环递增数组的最小值
题目: 已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:
- 若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
- 若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]
class Solution {
public int findMin(int[] nums) {
if (nums == null || nums.length == 0) {
throw new IllegalArgumentException("数组不能为空");
}
int left = 0, right = nums.length - 1;
while (left < right) { // 这里是 left < right,而不是 left <= right
int mid = left + (right - left) / 2;
if (nums[mid] > nums[right]) {
// 最小值一定在 mid 右侧
left = mid + 1;
} else {
// 最小值可能在 mid 或左侧
right = mid;
}
}
return nums[left]; // 最终 left == right,返回最小值
}
}
题目 : 已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,4,4,5,6,7] 在变化后可能得到:
- 若旋转 4 次,则可以得到 [4,5,6,7,0,1,4]
- 若旋转 7 次,则可以得到 [0,1,4,4,5,6,7]
class Solution {
public int findMin(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int left = 0, right = nums.length - 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] > nums[right]) {
left = mid + 1;
}else if (nums[mid] < nums[right]) {
right = mid;
}else {
// nums[mid] == nums[right]
right--;
}
}
return nums[left];
}
}
数组中的逆序对
题目 :在股票交易中,如果前一天的股价高于后一天的股价,则可以认为存在一个「交易逆序对」。请设计一个程序,输入一段时间内的股票交易记录 record,返回其中存在的「交易逆序对」总数。
class Solution {
public int reversePairs(int[] record) {
if (record == null || record.length < 2) return 0;
mergeSort(record, 0, record.length - 1);
return count;
}
public void mergeSort(int[] record, int left, int right) {
if (left >= right) return;
int mid = left + (right-left)/2;
mergeSort(record, left, mid);
mergeSort(record, mid+1, right);
// 合并
merge(record, left, mid, right);
}
int count = 0;
public void merge(int[] record, int left, int mid, int right) {
int[] temp = new int[right - left +1];
int i = left, j = mid+1;
int k = 0;
while (i <= mid && j <= right) {
if (record[i] <= record[j]) {
temp[k++] = record[i++];
}else {
//当左边数组的大与右边数组的元素时,就对当前元素以及后面的元素的个数进行统计,
//此时这个数就是,逆序数
//定义一个计数器,记下每次合并中存在的逆序数。
count += mid - i + 1;
temp[k++] = record[j++];
}
}
while (i <= mid) temp[k++] = record[i++];
while (j <= right) temp[k++] = record[j++];
//将新数组中的元素,覆盖nums旧数组中的元素。
//此时数组的元素已经是有序的
for(int t =0; t< temp.length;t++){
record[left+t] = temp[t];
}
}
}
如何找到一个无序数组的中位数
中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。
例如 arr = [2,3,4] 的中位数是 3 。
例如 arr = [2,3] 的中位数是 (2 + 3) / 2 = 2.5 。
实现 MedianFinder
类:
MedianFinder()
初始化 MedianFinder
对象。
void addNum(int num)
将数据流中的整数 num
添加到数据结构中。
double findMedian()
返回到目前为止所有元素的中位数。与实际答案相差 10-5 以内的答案将被接受。
class MedianFinder {
PriorityQueue<Integer> left;
PriorityQueue<Integer> right;
public MedianFinder() {
left = new PriorityQueue<>((a,b)->b-a); // 最大堆
right = new PriorityQueue<>(); // 最小堆
}
public void addNum(int num) {
if (left.size() == right.size()) {
right.offer(num);
left.offer(right.poll());
} else {
left.offer(num);
right.offer(left.poll());
}
}
public double findMedian() {
if (left.size() > right.size()) {
return left.peek();
}
return (left.peek() + right.peek()) / 2.0;
}
}
链表排序
class Solution {
public ListNode sortList(ListNode head) {
if (head == null || head.next == null) return head;
// 归并排序
ListNode slow = head;
ListNode fast = head.next;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
ListNode newHead = slow.next;
slow.next = null;
ListNode left = sortList(head);
ListNode right = sortList(newHead);
ListNode dm = new ListNode(0);
ListNode curr = dm;
while (left != null && right != null) {
if (left.val < right.val) {
curr.next = left;
left = left.next;
curr = curr.next;
}else {
curr.next = right;
right = right.next;
curr = curr.next;
}
}
if (left != null) {
curr.next = left;
}
if (right != null) {
curr.next = right;
}
return dm.next;
}
}
从一大段文本中找出TOP K 的高频词汇
题目: 给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。
class Solution {
public int[] topKFrequent(int[] nums, int k) {
PriorityQueue<int[]> pq = new PriorityQueue<>((a,b)->a[1]-b[1]);
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
map.put(nums[i], map.getOrDefault(nums[i], 0)+1);
}
for (int key : map.keySet()) {
if (pq.size() < k) {
pq.offer(new int[]{key, map.get(key)});
}else {
if (pq.peek()[1] < map.get(key)) {
pq.poll();
pq.offer(new int[]{key, map.get(key)});
}
}
}
int[] ans = new int[k];
for (int i = 0; i < k; i++) {
ans[i] = pq.poll()[0];
}
return ans;
}
}
题目: 给定一个单词列表 words 和一个整数 k ,返回前 k 个出现次数最多的单词。
返回的答案应该按单词出现频率由高到低排序。如果不同的单词有相同出现频率, 按字典顺序 排序。
class Solution {
public List<String> topKFrequent(String[] words, int k) {
Map<String, Integer> map = new HashMap<>();
for (int i = 0; i < words.length; i++) {
map.put(words[i], map.getOrDefault(words[i], 0)+1);
}
PriorityQueue<String> pq = new PriorityQueue<>((a,b)->{
//如果不同的单词有相同出现频率, 按字典顺序 排序
if (map.get(a) == map.get(b)) {
return b.compareTo(a);
}
return map.get(a) - map.get(b);
});
for (String s:map.keySet()) {
pq.offer(s);
if (pq.size() > k) {
pq.poll();
}
}
String[] ans = new String[k];
for (int i = k-1; i >= 0; i--) {
ans[i] = pq.poll();
}
return Arrays.asList(ans);
}
}