news 2026/5/8 10:16:04

字节 LeetCode 912.排序数组

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
字节 LeetCode 912.排序数组

前置题目:可先学习相对更简单的快速选择算法,重点掌握划分(partition)的原理,题目见hot100 215.数组中的第K个最大元素。

思路:手写快排。

1.快速排序的核心思想:

(1)定义一个递归函数sort(left,right),表示把子数组[left,right]升序排列。

(2)划分子数组[left,right],划分方法同hot100 215.数组中的第K个最大元素。设划分后,基准元素pivot的下标为i。

(a)在pivot左侧的元素,继续递归排序,即递归调用sort(left,i - 1)

(b)在pivot右侧的元素,继续递归排序,即递归调用sort(i + 1,right)

(3)递归边界:如果left >= right,说明子数组为空或者只剩一个元素,此时无需排序,直接返回

(4)递归入口:sort(0,n - 1)

2.优化:如果子数组已是升序数组,直接返回。

3.疑问:

(1)如果不随机选择基准元素pivot,会发生什么?

答:

如果子数组是降序的,且我们每次都选子数组的第一个(或者最后一个)元素作为pivot,那么按照算法,j不变或者移动到最左边,划分是最不均匀的,算法会退化至O(n^2)。随机选择pivot的目的是能使划分在期望意义上是均匀的(j移动到子数组的中间),保证算法的期望时间复杂度为O(nlogn)。

(2)代码中的nums[i] < pivot和nums[i] > pivot,能否改成nums[i] <= pivot和nums[i] >= pivot?

答:

这个做法会在子数组所有元素相同时,划分后的j是子数组的最后一个元素的下标,是最不均匀的划分,算法也会退化至O(n^2)。

(3)代码中的i <= j能否改成i < j?

答:

这样的话会算错。来看一个例子nums = [2,1,3],pivot = 2。

(a)左指针i = 1移动到i = 2,右指针j = 2因为不满足i < j的条件,无法移动。此时我们交换2和nums[j] = 3,得到[3,1,2],返回j = 2。然而j = 2的左侧有大于pivot = 2的元素,划分失败。

(b)如果写成i <= j,那么最终i = 2,j = 1。此时我们交换2和nums[j] = 1,得到[1,2,3],返回j = 1。这样的划分就是正确的。

4.复杂度分析:

(1)期望时间复杂度:O(nlogn),其中n是nums的长度。在平均情况下,可以视作均匀划分(partition)。递归深度为O(logn),每一层都会涉及O(n)个元素的比较和交换,所以时间复杂度等于一个高为O(logn),底边长为O(n)的矩形的面积,即O(nlogn)。

(2)期望空间复杂度:O(logn)。递归需要期望O(logn)的栈开销。

5.总结:

附代码:

class Solution { private static final Random rand = new Random(); public int[] sortArray(int[] nums) { quickSort(nums,0,nums.length - 1); return nums; } // 快速排序子数组[left,right] private void quickSort(int[] nums,int left,int right){ // 优化:如果子数组已经升序,直接返回 boolean ordered = true; for(int i = left;i < right;i++){ if(nums[i] > nums[i + 1]){ ordered = false; break; } } if(ordered){ return; } // 接收pivot最终的位置,满足pivot前面的元素都小于pivot,pivot后面的元素都大于pivot int i = partition(nums,left,right); quickSort(nums,left,i - 1); // 排序在 pivot 左侧的元素 quickSort(nums,i + 1,right); // 排序在 pivot 右侧的元素 } // 在子数组[left,right]中随机选择一个基准元素pivot // 根据pivot重新排列子数组[left,right] // 重新排列后,<= pivot的元素都在pivot的左侧,>= pivot的元素都在pivot的右侧 // 返回pivot在重新排列后的nums中的下标 // 特别地,如果子数组的所有元素都等于pivot,我们会返回子数组的中心下标,避免退化 private int partition(int[] nums,int left,int right){ // 1.在子数组[left,right]中随机选择一个基准元素pivot // rand.nextInt(int bound)表示生成一个范围在[0,bound)之间的随机整数 int i = left + rand.nextInt(right - left + 1); int pivot = nums[i]; // 把pivot与子数组第一个元素交换,避免pivot干扰后续划分,从而简化实现逻辑 swap(nums,i,left); // 2. 相向双指针遍历子数组 [left + 1, right] // 循环不变量:在循环过程中,子数组的数据分布始终如下图 // [ pivot | <=pivot | 尚未遍历 | >=pivot ] // ^ ^ ^ ^ // left i j right i = left + 1; int j = right; // 每走一步都会:i++或者j-- // 数组长度有限,因此也不会无限循环下去 while(true){ while(i <= j && nums[i] < pivot){ i++; } // 此时nums[i] >= pivot while(i <= j && nums[j] > pivot){ j--; } // 此时nums[j] <= pivot if(i >= j){ break; } // 维持循环不变量 swap(nums,i,j); i++; j--; } // 循环结束后 // [ pivot | <=pivot | >=pivot ] // ^ ^ ^ ^ // left j i right // 3. 把 pivot 与 nums[j] 交换,完成划分(partition) // 为什么与 j 交换? // 如果与 i 交换,可能会出现 i = right + 1 的情况,已经下标越界了,无法交换 // 另一个原因是如果 nums[i] > pivot,交换会导致一个大于 pivot 的数出现在子数组最左边,不是有效划分 // 与 j 交换,即使 j = left,交换也不会出错 swap(nums,left,j); // 交换后 // [ <=pivot | pivot | >=pivot ] // ^ // j // 返回 pivot 的下标 return j; } // 交换 nums[i] 与 nums[j],即索引位置对应的元素值 private void swap(int[] nums, int i, int j) { int tmp = nums[i]; nums[i] = nums[j]; nums[j] = tmp; } }

ACM模式:

import java.util.Random; import java.util.Scanner; class Solution { private static final Random rand = new Random(); public int[] sortArray(int[] nums) { quickSort(nums, 0, nums.length - 1); return nums; } // 快速排序子数组[left,right] private void quickSort(int[] nums, int left, int right) { // 优化:如果子数组已经升序,直接返回 boolean ordered = true; for (int i = left; i < right; i++) { if (nums[i] > nums[i + 1]) { ordered = false; break; } } if (ordered) { return; } // 接收pivot最终的位置,满足pivot前面的元素都小于pivot,pivot后面的元素都大于pivot int i = partition(nums, left, right); quickSort(nums, left, i - 1); quickSort(nums, i + 1, right); } // 在子数组[left,right]中随机选择一个基准元素pivot // 根据pivot重新排列子数组[left,right] // 重新排列后,<= pivot的元素都在pivot的左侧,>= pivot的元素都在pivot的右侧 // 返回pivot在重新排列后的nums中的下标 private int partition(int[] nums, int left, int right) { // 1.在子数组[left,right]中随机选择一个基准元素pivot int i = left + rand.nextInt(right - left + 1); int pivot = nums[i]; // 把pivot与子数组第一个元素交换 swap(nums, i, left); // 2. 相向双指针遍历子数组 [left + 1, right] i = left + 1; int j = right; while (true) { while (i <= j && nums[i] < pivot) { i++; } while (i <= j && nums[j] > pivot) { j--; } if (i >= j) { break; } swap(nums, i, j); i++; j--; } // 3. 把 pivot 与 nums[j] 交换,完成划分 swap(nums, left, j); return j; } // 交换 nums[i] 与 nums[j] private void swap(int[] nums, int i, int j) { int tmp = nums[i]; nums[i] = nums[j]; nums[j] = tmp; } } public class Main { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); // 读取数组长度 int n = scanner.nextInt(); // 读取数组元素 int[] nums = new int[n]; for (int i = 0; i < n; i++) { nums[i] = scanner.nextInt(); } // 排序数组 Solution solution = new Solution(); int[] result = solution.sortArray(nums); // 输出排序后的数组 for (int i = 0; i < result.length; i++) { System.out.print(result[i]); if (i < result.length - 1) { System.out.print(" "); } } scanner.close(); } }
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/8 10:16:02

考公行测图形推理:用‘箭头法’搞定六面体四面体,保姆级避坑教程

考公行测图形推理&#xff1a;用‘箭头法’搞定六面体四面体&#xff0c;保姆级避坑教程 图形推理作为行测考试中的高频题型&#xff0c;常常让考生感到头疼。尤其是立体图形推理中的六面体和四面体空间重构题&#xff0c;更是让不少考生在考场上手足无措。本文将详细介绍一种高…

作者头像 李华
网站建设 2026/5/8 10:16:00

私有化部署ChatGPT Web客户端:从架构原理到Docker实战

1. 项目概述&#xff1a;一个开源的Web版ChatGPT客户端最近在折腾AI应用的时候&#xff0c;发现了一个挺有意思的开源项目&#xff0c;叫xqdoo00o/chatgpt-web。简单来说&#xff0c;这就是一个让你能在自己的服务器上&#xff0c;通过一个漂亮的网页界面来使用ChatGPT API的工…

作者头像 李华
网站建设 2026/5/8 10:15:53

DLSS Swapper:一键升级游戏画质的智能管家,让你的显卡性能飙升

DLSS Swapper&#xff1a;一键升级游戏画质的智能管家&#xff0c;让你的显卡性能飙升 【免费下载链接】dlss-swapper 项目地址: https://gitcode.com/GitHub_Trending/dl/dlss-swapper DLSS Swapper 是一款专为游戏玩家打造的开源工具&#xff0c;能够自动检测并管理游…

作者头像 李华
网站建设 2026/5/8 10:15:43

揭秘书匠策AI:毕业论文创作的“全能智囊团”

在学术的征途上&#xff0c;毕业论文无疑是一座需要跨越的高山&#xff0c;它不仅考验着我们的知识储备&#xff0c;更挑战着我们的研究能力和写作技巧。面对这座高山&#xff0c;许多学子常常感到力不从心&#xff0c;从选题到定稿&#xff0c;每一步都充满了未知和挑战。但别…

作者头像 李华