阿里面试官:你连个排序算法都讲不明白?出门右拐吧
zhezhongyun 2025-07-24 23:20 25 浏览
排序算法一表总览
其他注意事项:
- 计数排序中,k kk是整数的范围
- 稳定性是指,序列中相同的数是否有可能交换顺序,例如序列中有两个8,顺序为8 88和8 ′ 8^{'}8′,如果在排序完之后,顺序有可能变为8 ′ 8^{'}8′和8 88,那么这种排序就是不稳定的排序算法;若不可能改变顺序,则是稳定算法。
- 只有归并排序可用于外部排序
- 在下面的代码中,swap函数用于元素交换,实现为:
private static void swap(int nums, int i, int j){
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
各排序算法说明及其实现
快速排序
- 图示:
- 原理:选择一个轴元素key,通过i j ijij指针定位元素,将待排序数组分为左侧小于等于key和右边大于等于key两组,递归操作。
- 时间复杂度:二分思想,故平均复杂度O ( n log n ) O(n \log n)O(nlogn);但如果每次二分选择的轴都极不平均则更高,极端举例:只把元素分为1和n - 1 n-1n-1个,则出现需要划分n次,而每次都要遍历数组,因此最坏复杂度为O ( n 2 ) O(n^2)O(n2)。例如完全正序,完全逆序都将出现最坏情况
- 空间复杂度:只有轴需要消耗空间,而二分法,每个部分都需要一个轴,因此空间为 O ( log n ) O(\log n)O(logn)
- 稳定性:根据图示可知,当i j ijij都定位到轴元素key时,将发生位置交换,因此不具有稳定性
- 参考代码
public static void quickSort(int[] nums, int l, int r){
if(l >= r) return;
int i = l - 1, j = r + 1; //先定位到边界两侧
int key = nums[l];
while(i < j){
while(nums[++i] < key); //先移动再与关键字判断
while(nums[--j] > key); //先移动在与关键字判断
if(i < j)
swap(nums, i, j); //交换两侧值
}
quickSort(nums, l, j);
quickSort(nums, j + 1, r);
}
堆排序
- 图示:较为复杂,暂无
- 原理:建立大顶堆,循环n nn次,每次:交换堆顶元素与堆尾元素使得堆中最大元素归于排序的正确位置,之后通过向下调整堆来完成堆的重构。
- 时间复杂度:每次调整堆显然需要经过树高h = log n h = \log nh=logn次比较,总共有n nn个数,需要进行n nn轮次调整堆,因此复杂度为O ( n log n ) O(n \log n)O(nlogn)。另外,通过向下调整建堆时间复杂度也是O ( n log n ) O(n \log n)O(nlogn),而若通过向上调整建堆时间复杂度可将降到O ( n ) O(n)O(n)
- 空间复杂度:消耗常数个空间用于存放比较时的临时变量
- 稳定性:堆尾元素会被放到堆顶用于向下调整,而无论向下调整堆是怎样的比较方案,都无法保证相同的元素不会被交换其原有位置。
- 参考代码
public static void heapSort(int[] nums){
for (int i = (nums.length >>> 1) - 1; i >= 0; i--){ //建堆
siftDown(nums, i, nums.length);
}
for(int i = nums.length - 1; i > 0; i--){ //排序
swap(nums, 0, i);
siftDown(nums, 0, i);
}
}
private static void siftDown(int[] heap, int i, int len){
int curNum = heap[i];
int half = len >>> 1;
while (i < half){ //直到到没有子结点
int lcIdx = (i << 1) + 1; //左子结点索引
int rcIdx = lcIdx + 1; //右子结点索引
int temp = heap[lcIdx]; //选取左子结点作为临时值
if(rcIdx < len && temp < heap[rcIdx]){
temp = heap[lcIdx = rcIdx];
}
if (curNum >= temp)
break;
heap[i] = temp;
i = lcIdx; //下一层检测
}
heap[i] = curNum;
}
归并排序
- 图示:
- 原理:对于已排好序的两个数组合并,只需要通过两个数组各自的指针i j ijij进行遍历,将比较结果放入新数组中,之后移动指针即可。归并排序通过上述理论,首先将数组两两划分,直到划分到长为1(有序),之后两两合并完成操作。
- 时间复杂度:典型分治,时间复杂度O ( n log n ) O(n \log n)O(nlogn)
- 空间复杂度:合并时开辟一个存放合并结果的辅助数组,数组长度只需要和原数组长度一致即可,因此为O ( n ) O(n)O(n)
- 稳定性:合并时不会影响次序,具有稳定性
- 参考代码
public void mergeSort(int nums, int l, int r){
if(l >= r) return;
int mid = (l + r) >> 1;
mergeSort(nums, l, mid);
mergeSort(nums. mid + 1, r);
int k = 0, i = l, j = mid + 1;
while (i <= mid && j <= r){
if (nums[i] <= nums[j])
temp[k++] = nums[i++];
else
temp[k++] = nums[j++];
}
//temp是外部开好的数组
while (i <= mid) temp[k++] = nums[i++];
while (j <= r) temp[k++] = nums[j++];
for (i = l, j = 0; i <= r; i++, j++)
nums[i] = temp[j];
}
选择排序
- 图示:
- 原理:一轮遍历可以找到一个数组的最小值,选择排序的方式就是每一轮都找到一个数组的最小值,是最容易理解的排序方法。
- 时间复杂度:需要n / 2 n/2n/2次遍历,每次遍历平均需要搜索n / 2 n/2n/2个元素,因此,时间复杂度O ( n ) O(n)O(n)
- 空间复杂度:需要常数辅助空间作为比较元素的临时遍历
- 稳定性:每次选择都是从前往后选,因此可以是稳定的
- 参考代码
public static void selectSort(int nums){
for(int i = 0; i < nums.length; i++){
int idx = i;
for(int j = i; j < nums.length; j++){
if(nums[j] < nums[i])
idx = j;
}
swap(nums, i, idx);
}
}
插入排序
- 图示:
- 原理:在排好顺中的序列中,可以通过遍历找到某个待插入正确位置使得新数组是有序的,因此,插入排序从前往后,依次将元素插入已排好序的数组中即可。(一个元素的数组可以看作有序数组)
- 时间复杂度:查找元素和插入平均都需要n / 2 n/2n/2次操作,因此,时间复杂度O ( n 2 ) O(n^2)O(n2);若数组已经升序,则只需要进行n - 1 n-1n-1次比较,因此最好时间复杂度为O ( n ) O(n)O(n),二分查找优化可降到O ( log n ) O(\log n)O(logn)
- 空间复杂度:比较后插入时需要常数个辅助空间存放临时变量
- 稳定性:可以是稳定的,设置好比较方法使得相同时插入在后面即可
- 参考代码
public static void insertSort(int nums){
for(int i = 1; i < nums.length; i++){
int temp = nums[i];
for(int j = i; j > 0 && nums[j - 1] > temp; j--){
nums[j] = nums[j - 1];
}
nums[j] = temp;
}
}
- 二分查找插入排序(即查找插入位置时使用二分法优化)
public static void insertSort(int nums){
for(int i = 1; i < nums.length; i++){
int temp = nums[i];
int l = 0, r = i - 1;
while(l <= r){
int mid = (l + r)/2;
if(nums[mid] > temp) r = mid - 1;
else l = mid + 1;
}
for(int j = i; j > low; j--){
nums[j] = nums[j - 1];
}
nums[j] = temp;
}
}
希尔排序
- 图示:
- 原理:插入排序的改进办法,通过缩小增量的插入排序来降低时间复杂度,gap开始为数组的一半,每次减半,每轮都完成一次跨越gap的插入排序。
- 时间复杂度:O ( n 1.3 ) O(n^{1.3})O(n1.3),较难证明,可以写作O ( n log n ) O(n\log n)O(nlogn)
- 空间复杂度:同时间复杂度
- 稳定性:由于分组的存在,不稳定
- 参考代码
public static void shellSort(int[] nums){
int gap = nums.length;
while(true){
gap /= 2;
for(int i = 0; i < gap; i++){
temp = nums[i];
for(int j = i; j > 0 && temp > nums[j]; j -= gap){
nums[j] = nums[j - gap];
}
nums[i] = temp;
}
if(gap == 1) break;
}
}
冒泡排序
- 图示:
- 原理:”车轮式的比武,每次决出胜者将参与下一次比武,直到选出最强者;每次选出剩余角色的最强者都需要进行一轮比武"
- 时间复杂度:由原理易知为O ( n 2 ) O(n^2)O(n2)
- 空间复杂度:常数
- 稳定性:稳定
- 参考代码
public static void bubbleSort(int nums){
for(int i = 1; i < nums.length; i++){
boolean changed = false;
for(int j = 0; j < nums.length - i;j++){
if(nums[j] > nums[j + 1]){
swap(nums, j, j + 1);
changed = true;
}
}
if(changed == false) break;
}
}
- 双向冒泡(时间有所优化,又称“鸡尾酒排序”)
public static void bubbleSort(int nums){
int l = 0, r = nums.length - 1, shift = 1;
while(l < r){
boolean changed = false;
for(int i = l; i < r; i++){
if(nums[i] > nums[i + 1]){
swap(nums, i, i + 1);
shift = i;
}
}
r = shift;
for(int i = r - 1; i >= l; i--){
if(nums[i] > nums[i + 1]){
swap(nums, i, i + 1);
shift = i + 1;
}
}
l = shift;
}
}
计数排序
- 图示:较为简单,故无图示
- 原理:统计数字出现次数即可
- 时间复杂度:与整数范围有关
- 空间复杂度:与整数范围有关
- 稳定性:稳定
- 参考代码
public static void countingSort(int nums){
int[] counter = new int[65535];
//此处的counter数组需要随情况变化
for(int i = 0; i < nums.length; i++){
counter[nums[i]]++;
}
int idx = 0;
for(int i = 0; i < counter.length; i++){
while(counter[i] > 0)
nums[idx++] = counter[i];
}
}
- 局限性:只能排序整数;整数范围较大则开辟空间较多
排序算法的应用举例
刷题需要掌握的几种算法
- 快速排序与归并排序的算法思想:分支。
分治就是把一个复杂的问题分成两个或更多的相同或相似的子问题,直到最后子问题可以简单地直接求解,原问题的解即子问题的解的合并。显然到最后,快速排序的简单拼接和归并排序的有序数组合并都属于这里所说的简单问题。 - 堆排序的核心优势–求k个最值。
前面我们说到,通过向上调整建堆时间复杂度可降低到O ( n ) O(n)O(n),而每次向下调整堆的时间复杂度在O ( log n ) O(\log n)O(logn)。因此,求k个最值用堆排序,即向上调整建堆,取得堆顶最值,向下调整堆,继续取堆顶最值,循环往复。如此一来,当k较小( < log n ) (<\log n)(<logn)时,求前k个最值时间复杂度即为建堆复杂度O ( n ) O(n)O(n) - 已知元素范围的最佳排序–计数排序
其实在很多时候,我们待排序的数的范围已知并且最大最小值相差不大时,计数排序是最优秀的算法
leetcode排序算法题举例
- leetcode[75] 颜色分类 【中等】:
给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。 - 【思路】典型的counting sort应用,由于红、白、蓝分别是0,1,2,因此待排序数组是整数,而且范围0-2很小,可以使用计数排序,而且原地排序符合要求。
class Solution {
public void sortColors(int[] nums) {
int[] counter = new int[3];
for(int i = 0; i < nums.length; i++){
counter[nums[i]]++;
}
int idx = 0;
for(int i = 0; i < 3; i++){
while(counter[i] > 0){
nums[idx++] = i;
counter[i]--;
}
}
}
}
- leetcode [1366] 通过投票对团队排名【中等】
现在有一个特殊的排名系统,依据参赛团队在投票人心中的次序进行排名,每个投票者都需要按从高到低的顺序对参与排名的所有团队进行排位。
排名规则如下:
参赛团队的排名次序依照其所获「排位第一」的票的多少决定。如果存在多个团队并列的情况,将继续考虑其「排位第二」的票的数量。以此类推,直到不再存在并列的情况。
如果在考虑完所有投票情况后仍然出现并列现象,则根据团队字母的字母顺序进行排名。
给你一个字符串数组 votes 代表全体投票者给出的排位情况,请你根据上述排名规则对所有参赛团队进行排名。
请你返回能表示安排名系统 排序后 的所有团队排名的字符串。 - 【题解】这里提供一个计数排序方法,HashMap也可解。通过统计每一个团队的投票信息进行排序,最后得到排序结果打印即可。这里相当于使用一个大小26的数组统计字符个数,是一种常用方法。
class Solution {
public String rankTeams(String[] votes) {
int len = votes[0].length();
int[][] map = new int[26][len + 1]; // 多1用于存放团队信息
for(int i = 0; i < 26; i++) map[i][len] = i;
for(int i = 0; i < votes.length; i++){ //投票统计
String s = votes[i];
for(int j = 0; j < len; j++){
map[s.charAt(j) - 'A'][j]++;
}
}
Arrays.sort(map, (a, b) ->{ //投票结果排序
for(int i = 0; i < len; i++){
if(a[i] < b[i]) return 1;
if(a[i] > b[i]) return -1;
}
return 0;
});
StringBuilder sb = new StringBuilder();
for(int i = 0; i < len; i++){ //获取结果对应团队
sb.append((char)('A' + map[i][len]));
}
return sb.toString();
}
}
- leetcode[973] 最接近原点的 K 个点(堆排序的典型应用)
- leetcode[1471] 数组中的 k 个最强值(堆排序的典型应用)
- leetcode[1481] Least Number of Unique Integers after K Removals(堆排序的典型应用)
- leetcode[179] 最大数(排序规则设计)
- leetcode [242] Valid Anagram(排序规则设计)
- leetcode [524] Longest Word in Dictionary through Deleting(排序规则设计)
- leetcode [976] Largest Perimeter Triangle(排序规则设计)
- leetcode [1451] Rearrange Words in a Sentence(排序规则设计)
- leetcode [853] Car Fleet
N 辆车沿着一条车道驶向位于 target 英里之外的共同目的地。
每辆车 i 以恒定的速度 speed[i] (英里/小时),从初始位置 position[i] (英里) 沿车道驶向目的地。
一辆车永远不会超过前面的另一辆车,但它可以追上去,并与前车以相同的速度紧接着行驶。
此时,我们会忽略这两辆车之间的距离,也就是说,它们被假定处于相同的位置。
车队 是一些由行驶在相同位置、具有相同速度的车组成的非空集合。注意,一辆车也可以是一个车队。
即便一辆车在目的地才赶上了一个车队,它们仍然会被视作是同一个车队。 - 【题解】此题也是排序规则设计,基于这样一个事实:如果某辆车起点在前面,但(如果单独行动)到达目的地的时间比起点在后面的车晚,则可以说明被超车,会被组成车队;因此计算到达花费时间,排序,然后统计车队数量即可
class Solution {
public int carFleet(int target, int[] position, int[] speed) {
int n = position.length, res = 0;
double[][] cars = new double[n][2];
//分别记录开始位置和到达时间
for(int i = 0; i < n; i++){
cars[i][0] = position[i];
cars[i][1] = (double)(target - position[i])/speed[i];
}
//开始位置排序
Arrays.sort(cars, (a, b) -> Double.compare(a[0], b[0]));
double cur = 0;
for(int i = n - 1; i >= 0 ; i--){
//能否追上前车
if(cars[i][1] > cur){
cur = cars[i][1];
res++;
}
}
return res;
}
}
小结
- 基本有序时或逆序时快速排序慢;基本有序时插入、希尔、冒泡快;逆序时冒泡排序慢
- “分组错乱”的方法都不稳定
- 平均时间O ( n log n ) O(n \log n)O(nlogn)方法优缺点要明确:快排速度最快,但可能出现较慢情况,不稳定;归并稳定且可用于外部排序,但消耗空间大;堆排序适用于排前k个,不稳定
- 计数排序很好用,若是整数且范围小,请用计数排序
- 求前k个用堆排序或选择排序
- 快排核心在于划分思想
相关推荐
- Opinion丨Struggle Against U.S. Mind colonization in the Global South
-
Editor'snote:Thismonth,XinhuaNewsAgency'sThinkTankreleasedareporttitled"Colonizationof...
- 爱可可AI论文推介(2020.11.4)_爱可可女装旗舰店
-
LG-机器学习CV-计算机视觉CL-计算与语言AS-音频与语音RO-机器人(*表示值得重点关注)1、[LG]*CombiningLabelPropagationan...
- 何新:罗马伪史考英文版序言_罗马史学
-
2019-10-2514:48:27何新:罗马伪史考序言(英文译本)HeXin:PreambleofResearchonPseudo-historyofRome1Afewyear...
- XPeng Stock Rises Over 4% after Q2 Revenue and EV Margin Set Records
-
TMTPOST--TheAmericandepositaryreceipts(ADRs)ofXPengInc.rosearound4.2%onTuesdayaftert...
- 英汉世界语部首(八)_英文部首字典
-
本节讲八个部首,分别是:弓gōng【ECWLrad】bow廾gǒng【ECWLrad】twen广guǎng【ECWLrad】vast己jǐ【ECWLrad】self已yǐ...
- 一课译词:划水_划水是什么地方的方言
-
[Photo/SIPA]懒惰是人类的天性,因此才总有人会在工作时“划水”。“划水【huáshuǐ】”,本意是指“用胳膊划的动作(makestrokeswithone’sarms)”,延伸为“...
- 首测!GPT-4o做Code Review可行吗?
-
编辑|言征出品|51CTO技术栈(微信号:blog51cto)近日,OpenAI一记重拳,推出了GPT-4o(“o”表示“omni”),将语音识别和对话方面的优势展示的淋漓尽致。几乎可以肯定,...
- C++|漫谈STL细节及内部原理_c++ stl详解
-
1988年,AlexanderStepanov开始进入惠普的PaloAlto实验室工作,在随后的4年中,他从事的是有关磁盘驱动器方面的工作。直到1992年,由于参加并主持了实验室主任BillWo...
- C++ inline关键字深度解析:不止于优化的头文件定义许可
-
在C++开发中,几乎每个程序员都用过inline关键字,但多数人只停留在“内联优化”的表层理解。事实上,inline的真正威力在于它打破了C++的单一定义规则(ODR)限制,成为头文件中安全定义函数的...
- 实用 | 10分钟教你搭建一个嵌入式web服务器
-
之前分享的文章中提到了几种可以在嵌入式中使用的web服务器。嵌入式web服务器就是把web服务器移植到嵌入式系统的服务器。它仍然是基于http文本协议进行通信的,具有标准的接口形式,对客户端...
- 中间语言格式_中间格式文本是什么
-
在通常情况下,编译器会将目标语言转换成某种中间语言格式,而不是直接将源代码转换成二进制机器指令,不少c语言编译器,都会将代码编译成汇编语言,然后再通过汇编语言编译器将汇编代码转换成目标机器可执行的二进...
- 一线开发大牛带你深度解析探讨模板解释器,解释器的生成
-
解释器生成解释器的机器代码片段都是在TemplateInterpreterGenerator::generate_all()中生成的,下面将分小节详细展示该函数的具体细节,以及解释器某个组件的机器代码...
- 干货,Web开发和前端开发逆天工具大全
-
微信ID:WEB_wysj(点击关注)◎◎◎◎◎◎◎◎◎一┳═┻︻▄(点击页底“阅读原文”前往下载)●●●逆天工具CDN资源库国内Bootstrap中文网开源项目免费CDN服务36...
- 移动端rem+vw适配_移动端web页面适配方案
-
rem:rem是相对单位,设置根元素html的font-size,比如给html设置字体大小为100px,1rem=100px;rem缺点:1.和根元素font-size值强耦合,系统字...
- 从零搭建 React 开发 H5 模板_react html5
-
项目创建创建项目文件夹mkdir react-democd react-demonpm init -y依赖安装yarn add rea...
- 一周热门
- 最近发表
-
- Opinion丨Struggle Against U.S. Mind colonization in the Global South
- 爱可可AI论文推介(2020.11.4)_爱可可女装旗舰店
- 何新:罗马伪史考英文版序言_罗马史学
- XPeng Stock Rises Over 4% after Q2 Revenue and EV Margin Set Records
- 英汉世界语部首(八)_英文部首字典
- 一课译词:划水_划水是什么地方的方言
- 首测!GPT-4o做Code Review可行吗?
- C++|漫谈STL细节及内部原理_c++ stl详解
- C++ inline关键字深度解析:不止于优化的头文件定义许可
- 实用 | 10分钟教你搭建一个嵌入式web服务器
- 标签列表
-
- HTML 教程 (33)
- HTML 简介 (35)
- HTML 实例/测验 (32)
- HTML 测验 (32)
- JavaScript 和 HTML DOM 参考手册 (32)
- HTML 拓展阅读 (30)
- HTML文本框样式 (31)
- HTML滚动条样式 (34)
- HTML5 浏览器支持 (33)
- HTML5 新元素 (33)
- HTML5 WebSocket (30)
- HTML5 代码规范 (32)
- HTML5 标签 (717)
- HTML5 标签 (已废弃) (75)
- HTML5电子书 (32)
- HTML5开发工具 (34)
- HTML5小游戏源码 (34)
- HTML5模板下载 (30)
- HTTP 状态消息 (33)
- HTTP 方法:GET 对比 POST (33)
- 键盘快捷键 (35)
- 标签 (226)
- HTML button formtarget 属性 (30)
- opacity 属性 (32)
- transition 属性 (33)