稳定排序和不稳定排序

原文:https://www.cnblogs.com/codingmylife/archive/2012/10/21/2732980.html

这几天笔试了好几次了,连续碰到一个关于常见排序算法稳定性判别的问题,往往还是多选,对于我以及和我一样拿不准的同学可不是一个能轻易下结论的题目,当然如果你笔试之前已经记住了数据结构书上哪些是稳定的,哪些不是稳定的,做起来应该可以轻松搞定。本文是针对老是记不住这个或者想真正明白到底为什么是稳定或者不稳定的人准备的。

首先,排序算法的稳定性大家应该都知道,通俗地讲就是能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。在简单形式化一下,如果Ai = Aj,Ai原来在位置前,排序后Ai还是要在Aj位置前。

其次,说一下稳定性的好处。排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。基数排序就是这样,先按低位排序,逐次按高位排序,低位相同的元素其顺序再高位也相同时是不会改变的。另外,如果排序算法稳定,对基于比较的排序算法而言,元素交换的次数可能会少一些(个人感觉,没有证实)。

回到主题,现在分析一下常见的排序算法的稳定性,每个都给出简单的理由。

(1)冒泡排序

冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,我想你是不会再无聊地把他们俩交换一下的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。

(2)选择排序

选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n - 1个元素,第n个元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果当前元素比一个元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。比较拗口,举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法。

(3)插入排序
插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。

(4)快速排序
快速排序有两个方向,左边的i下标一直往右走,当a[i] <= a[center_index],其中center_index是中枢元素的数组下标,一般取为数组第0个元素。而右边的j下标一直往左走,当a[j] > a[center_index]。如果i和j都走不动了,i <= j,交换a[i]和a[j],重复上面的过程,直到i > j。 交换a[j]和a[center_index],完成一趟快速排序。在中枢元素和a[j]交换的时候,很有可能把前面的元素的稳定性打乱,比如序列为5 3 3 4 3 8 9 10 11,现在中枢元素5和3(第5个元素,下标从1开始计)交换就会把元素3的稳定性打乱,所以快速排序是一个不稳定的排序算法,不稳定发生在中枢元素和a[j] 交换的时刻。

(5)归并排序
归并排序是把序列递归地分成短序列,递归出口是短序列只有1个元素(认为直接有序)或者2个序列(1次比较和交换),然后把各个有序的段序列合并成一个有序的长序列,不断合并直到原序列全部排好序。可以发现,在1个或2个元素时,1个元素不会交换,2个元素如果大小相等也没有人故意交换,这不会破坏稳定性。那么,在短的有序序列合并的过程中,稳定是是否受到破坏?没有,合并过程中我们可以保证如果两个当前元素相等时,我们把处在前面的序列的元素保存在结果序列的前面,这样就保证了稳定性。所以,归并排序也是稳定的排序算法。

(6)基数排序
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序,最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以其是稳定的排序算法。

(7)希尔排序(shell)
希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小, 插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比O(n^2)好一些。由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。

(8)堆排序
我们知道堆的结构是节点i的孩子为2 * i和2 * i + 1节点,大顶堆要求父节点大于等于其2个子节点,小顶堆要求父节点小于等于其2个子节点。在一个长为n 的序列,堆排序的过程是从第n / 2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n / 2 - 1, n / 2 - 2, … 1这些个父节点选择元素时,就会破坏稳定性。有可能第n / 2个父节点交换把后面一个元素交换过去了,而第n / 2 - 1个父节点把后面一个相同的元素没 有交换,那么这2个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法。

综上,得出结论: 选择排序、快速排序、希尔排序、堆排序不是稳定的排序算法,而冒泡排序、插入排序、归并排序和基数排序是稳定的排序算法

<think> 我们被要求提供稳定排序不稳定排序的列表说明。根据引用资料,稳定排序是指相等元素的相对顺序在排序后保持不变,而不稳定排序则可能改变相等元素的相对顺序。 常见的稳定排序算法包括: 1. 冒泡排序(Bubble Sort) 2. 插入排序(Insertion Sort) 3. 归并排序(Merge Sort) 4. 计数排序(Counting Sort) 5. 基数排序(Radix Sort) 6. 桶排序(Bucket Sort) 常见的不稳定排序算法包括: 1. 选择排序(Selection Sort) 2. 快速排序(Quick Sort) 3. 堆排序(Heap Sort) 4. 希尔排序(Shell Sort) 下面分别简要说明: ### 稳定排序算法 1. **冒泡排序**:相邻元素比较交换,相等的元素不会交换位置,因此稳定[^2]。 2. **插入排序**:将元素插入到已排序序列中的适当位置,遇到相等元素时插入到相等元素的后面,因此稳定[^2]。 3. **归并排序**:合并两个有序数组时,当遇到相等元素,优先取前一个数组的元素,从而保证稳定性[^1]。 4. **计数排序**:非比较排序,利用频数数组,然后按顺序输出,相等元素按输入顺序输出,因此稳定。 5. **基数排序**:基于稳定的子排序(通常使用计数排序),所以整体稳定[^4]。 6. **桶排序**:每个桶内使用稳定排序算法(如插入排序),然后按顺序合并桶,因此稳定。 ### 不稳定排序算法 1. **选择排序**:每次选择最小(或最大)元素与当前位置交换,可能破坏相等元素的相对顺序(例如,当前元素最小元素交换时,可能越过相等的元素)[^3]。 2. **快速排序**:分区过程中,基准元素与其它元素的交换可能导致相等元素的相对位置改变[^1]。 3. **堆排序**:在构建堆调整堆的过程中,可能打乱相等元素的相对顺序(例如,父节点子节点交换时)[^3][^4]。 4. **希尔排序**:分组进行插入排序,由于分组间隔的存在,相等元素可能被分到不同组,从而改变相对顺序。 注意:某些算法稳定性取决于具体实现。例如,归并排序在合并时如果遇到相等元素总是先取左边的元素,则是稳定的;如果比较时使用非稳定的条件(比如严格小于才取左边,否则取右边,但相等时取右边先出现),则可能变为不稳定。但通常实现都是稳定的。 下面是一个不稳定排序的例子(堆排序): 假设有序列:$[5a, 6, 3, 5b, 2]$(其中$5a$$5b$是值都为5的两个不同元素,ab用于区分原顺序)。 堆排序过程中,构建大顶堆时,$5a$$5b$的相对顺序可能被打乱。例如,在调整堆时,$5b$可能被交换到$5a$的前面。 而稳定排序(如归并排序)在合并子数组$[5a, 6]$$[3, 5b, 2]$(经过排序后为$[2,3,5b]$)时,合并过程会比较两个子数组的元素,当两个5比较时,会先取左边子数组的$5a$(因为相等时取左边),从而保持$5a$在$5b$前面。 总结:稳定排序在需要保持相等元素原始顺序的场景中非常重要(比如多关键字排序),而不稳定排序虽然可能改变相等元素的相对位置,但通常有更高的效率(如快速排序、堆排序的平均时间复杂度为$O(n\log n)$且常数因子较小)[^4][^5]。 如果需要具体代码示例,我们可以提供。 ```python # 稳定的插入排序实现 def insertion_sort(arr): for i in range(1, len(arr)): key = arr[i] j = i-1 while j >=0 and key < arr[j]: # 注意:如果这里改为<=,则变成不稳定 arr[j+1] = arr[j] j -= 1 arr[j+1] = key return arr # 不稳定的选择排序实现 def selection_sort(arr): for i in range(len(arr)): min_idx = i for j in range(i+1, len(arr)): if arr[j] < arr[min_idx]: min_idx = j arr[i], arr[min_idx] = arr[min_idx], arr[i] return arr ``` 在上面的插入排序中,我们使用`key < arr[j]`,这样当遇到相等的元素时就不会继续移动,从而保证稳定性。而在选择排序中,我们每次找到最小元素并当前位置交换,这个交换可能破坏稳定性。 §§相关问题§§ 1. 在哪些实际应用场景中必须使用稳定排序?[^5] 2. 如何修改快速排序使其成为稳定排序? (提示:增加额外索引) 3. 堆排序不稳定的具体例子是什么?[^3][^4] </think> ### 稳定排序不稳定排序的列表说明 在排序算法中,**稳定性**指相等元素在排序前后的相对顺序保持不变。以下是常见排序算法的分类及说明: #### 一、稳定排序算法列表 | 排序算法 | 基本原理 | 时间复杂度 | 稳定保证机制 | |----------------|------------------------------|------------------|-------------------------------------------------------------------------------| | **冒泡排序** | 相邻元素比较交换 | $O(n^2)$ | 遇到相等元素时**不交换**,保持原顺序[^1] | | **插入排序** | 元素插入已排序序列的适当位置 | $O(n^2)$ | 遇到相等元素时插入到**相同元素的末尾**,维持原顺序[^2] | | **归并排序** | 分治合并有序子数组 | $O(n \log n)$ | 合并时优先取**左侧数组**的相等元素(如 `left[i] <= right[j]` 先取 `left[i]`)[^1] | | **计数排序** | 统计元素频次重建序列 | $O(n+k)$ | 反向填充时**从后向前**遍历原数组,保留原始相对位置[^4] | | **基数排序** | 按位分配收集 | $O(d(n+k))$ | 依赖稳定的子排序(通常用计数排序)[^4] | | **桶排序** | 分桶后桶内排序 | $O(n+k)$ | 桶内使用稳定算法(如插入排序)[^5] | #### 二、不稳定排序算法列表 | 排序算法 | 基本原理 | 时间复杂度 | 不稳定性原因 | |----------------|------------------------------|------------------|-----------------------------------------------------------------------------| | **选择排序** | 选择最小元素交换到当前位置 | $O(n^2)$ | 交换时可能将**后方相等元素**越过前方相等元素(如 `[5a, 5b, 3] → [3, 5b, 5a]`)[^3] | | **快速排序** | 分区递归排序 | $O(n \log n)$ | 分区交换时**不保留相等元素的相对位置**(如基准值交换打乱顺序)[^1] | | **堆排序** | 构建堆结构交换堆顶元素 | $O(n \log n)$ | 堆调整时**父子节点交换**可能颠倒相等元素的顺序(如图例)[^3][^4] | | **希尔排序** | 分组插入排序 | $O(n \log n)$ | 分组间隔导致**相等元素被分到不同组**,相对顺序改变[^5] | --- ### 堆排序不稳定的图示示例 假设初始序列:$[5a, 6, 3, 5b, 2]$($5a$ $5b$ 值相同,下标区分原始位置) 1. **建堆过程**(大顶堆): - 调整节点时 $5a$ 与 $6$ 交换 → $[6, 5a, 3, 5b, 2]$ - 继续调整时 $5a$ 与 $5b$ 交换 → $[6, 5b, 3, 5a, 2]$ **此时 $5b$ 已移动到 $5a$ 前面** 2. **输出堆顶后重建堆**: - 输出 $6$ 后交换堆顶与末尾 → $[2, 5b, 3, 5a]$ - 重建堆时 $5b$ $5a$ 的相对顺序可能再次改变 最终结果可能为:$[2, 3, 5a, 5b, 6]$ 或 $[2, 3, 5b, 5a, 6]$ → **$5a$ $5b$ 顺序不稳定**[^3][^4] --- ### 稳定性的实际意义 1. **多关键字排序**: 先按次要键排序,再按主要键排序时,稳定算法能保留次要键的顺序(如先按学号排,再按成绩排)[^4]。 2. **用户界面展示**: 表格中多次点击不同列排序时,稳定性能维持上次排序的部分顺序[^5]。 3. **数据连续性**: 如时间序列数据中,相等值的原始顺序可能隐含重要信息(如事件发生先后)[^1]。 > 关键结论:**算法是否稳定取决于实现细节**(如比较条件、交换逻辑)。例如快速排序若用额外空间记录索引可变为稳定,但会增加空间复杂度[^4]。 ```python # 稳定排序的实现关键(归并排序合并逻辑) def merge(left, right): result = [] i = j = 0 while i < len(left) and j < len(right): # 关键:用 <= 而非 < 保证相等时优先取左侧元素 if left[i] <= right[j]: result.append(left[i]) i += 1 else: result.append(right[j]) j += 1 # 添加剩余元素 result.extend(left[i:]) result.extend(right[j:]) return result ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值