排序算法介绍
1. 基本概念
- 稳定性: 待排序的数列中,若两个元素的值相等 R1 = R2 ,在排序结束之后,元素之间的相对位置没有发生变化,则称排序算法是稳定的,否则是不稳定的。算法是否具有稳定性并不能衡量一个算法的优劣,它主要是对算法性质进行描述。
2. 内部排序
排序期间元素全部存放在内存中。
2.1 插入排序
每次将待排序的元素插入到前面已排好序的子序列中
2.1.1 直接插入排序
思路: 第 i 次排序,将第 i 个元素插入到前面 i - 1 个已排序好的子序列中
时间复杂度
- 最好情况:表中元素已经有序,每插入一个元素,都只需要比较一次而不需要移动元素,时间复杂度为 O(n)
- 最坏情况:表中元素顺序与结果中想要的顺序相反,那么比较次数就是 2+3+…+ n 次的和,时间复杂度为 O(n^2)
- 平均情况:最好情况和最坏情况的平均值 O(n^2)
空间复杂度: 仅使用了常数个辅助单元,空间复杂度为 O(1)
稳定性: 每次待插入的元素都是从后面往前面先比较后移动,若找到相同元素,相同位置也不会发生变化,即是稳定的排序算法
适用场景:元素有序,元素较少
代码实现:
public void fun3(int[] array){
if (array == null || array.length == 0){
return;
}
for (int i = 1; i < array.length ; i++) {
int temp = array[i];
int k = i-1;
while (k >= 0 && temp < array[k]){
array[k+1] = array[k];
k--;
}
array[k+1] = temp;
}
}
2.1.2 折半插入排序
思路: 与直接插入排序类似,只是查找元素待插入位置 k 的时候,使用的是折半查找
时间复杂度
- 只是减少了比较元素的次数,约为O(nlogn),但元素的移动次数仍然依赖于序列的原始状态,故时间复杂度情况与直接插入排序相同
空间复杂度: 仅使用了常数个辅助单元,空间复杂度为 O(1)
代码实现:
public void fun(int[] array){
if (array == null || array.length == 0){
return;
}
for (int i = 1; i < array.length; i++) {
int temp = array[i];
int low = 0;
int high = i-1;
while (low <= high){
int mid = (low + high) / 2;
if (temp < array[mid]){
high = mid - 1;
} else {
low = mid + 1;
}
}
for (int j = i; j > low; j--) {
array[j] = array[j-1];
}
array[low] = temp;
}
}
2.1.3 希尔排序
思路: 又称 缩小增量排序,将原序列分割为多个L [ i,i + d,i + 2d,+…+ i + kd ] 的子序列,满足这个间隔 d 的元素分配在同一个子序列,并进行直接插入排序,并缩小 d 值,待 d 值为 1 时,在进行最后一次直接插入排序,d 值初始值为 n / 2
时间复杂度
- 据说是数学难题,不好分析,最坏情况为O(n^2)
空间复杂度: 仅使用了常数个辅助单元,空间复杂度为 O(1)
稳定性: 相同元素可能被划分到不同子序列,相对位置可能发生变化,故为不稳定的排序算法
代码实现: 三层 for 循环 or 二层 for 循环
public void fun2(int[] array){
if (array == null || array.length == 0){
return;
}
for (int gap = array.length / 2;gap > 0;gap /= 2) {
for (int i = gap; i < array.length; i++) {
int temp = array[i];
int k = i-gap;
while (k >= 0 && temp < array[k]){
array[k+gap] = array[k];
k -= gap;
}
array[k+gap] = temp;
}
}
}
2.2 选择排序
在第 i 趟排序中,从后面的 n - i + 1 个待排序的元素中选取最小的元素作为第 i 个元素,直到第 n - 1 趟排序走完,只剩 1 个待排序的元素。注意,插入排序是将待排序的元素插入到前面已排序的序列中,选择排序是将后面子序列中选取最小(大)值放到最终位置
2.2.1 简单选择排序
思路: 原序列为L [1… n] ,在第 i 次排序中,从 子序列 L [i .. n] 中选取最小值与 L [i] 进行交换
时间复杂度
- 具体查看代码实现方式,O(n^2)
空间复杂度: 仅使用了常数个辅助单元,空间复杂度为 O(1)
稳定性: 不稳定,例如 5 8 5 2 9,第一次排序后,第一个 5 和 2 交换了位置。
代码实现:
public void fun(int[] array){
if (array == null || array.length <= 1){
return;
}
for (int i = 0; i < array.length; i++) {
int index = i;
for (int j = i+1; j < array.length; j++) {
if (array[j] < array[index]){
index = j;
}
}
if (index != i){
int temp = array[index];
array[index] = array[i];
array[i] = temp;
}
}
}
2.2.2 堆排序:这个比较复杂,后续再更新
思路: 将原序列按照层次遍历的方式构成完全二叉树,然后构建大根堆,大根堆满足父节点的值大于等于子节点的值,输入根节点的值,然后再进行堆调整。若节点 i 为中间节点,则父节点索引为 (i – 1) / 2,左右子结点索引分别为 2 i + 1 和 2 i + 2 。
时间复杂度
- 构建大根堆时间为O(n),之后有n-1次调整操作,每次调整时间复杂度为O(h),故最好、最坏、平均情况下时间复杂度为O(nlogn)
空间复杂度: 仅使用了常数个辅助单元,空间复杂度为 O(1)
稳定性:
代码实现:
2.3 交换排序
比较两个元素的关键值大小来交换它们的位置
2.3.1 冒泡排序
思路: 序列从索引 0 开始,元素之间两两比较,若为逆序则进行交换,最终能得到最大的值,然后再从左到右,进行比较交换,得到第二大的值,上一轮得到的数字不必进行比较,此排序每次能得到一个最小(大)值。
时间复杂度
- 最好情况:序列已经满足有序,无需交换,复杂度为O(n)
- 最坏情况:序列逆序,为O(n^2)
- 平均情况:O(n^2)
空间复杂度: 仅使用了常数个辅助单元,空间复杂度为 O(1)
稳定性: 若算法中,i > j 且 L[i] = L[j] 不交换,则算法是稳定的,依算法具体规则定
代码实现:
public void fun (int[] array){
for (int i = 0; i < array.length; i++) {
for (int j = 0; j < array.length - i - 1; j++) {
if (array[j] > array[j+1]){
int temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
}
}
}
2.3.2 快速排序
思路: 基于分治思想,在原序列 L 中选择任意一个元素作为基准,经过一趟排序之后将原序列分为两个部分 L [1…k-1] 和 L [k+1…n],其中 L [1…k-1] 内的元素值小于等于基准值,L [k+1…n] 内的元素值大于等于基准值,然后递归地对上述两个子序列进行上述操作,知道子序列中只含有一个元素值即可。
时间复杂度
- 最好情况:序列已经满足有序,无需交换,复杂度为O(n)
- 最坏情况:序列逆序,为O(n^2)
- 平均情况:O(n^2)
空间复杂度: 仅使用了常数个辅助单元,空间复杂度为 O(1)
稳定性: 若算法中,i > j 且 L[i] = L[j] 不交换,则算法是稳定的,依算法具体规则定
代码实现:
public void fun(int[] array,int low,int high){
if (high <= low){
return;
}
int i = low;
int j = high;
int temp = array[i];
while(i < j){
// 注意后面判断条件,不能都只 > temp,否则遇到 array[i] == array[j] 时会进入死循环。
while(i<j && array[j] >= temp){
j--;
}
if (i < j){
array[i] = array[j];
}
while(i<j && array[i] < temp){
i++;
}
if (i < j){
array[j] = array[i];
}
}
array[i] = temp;
fun(array,low,i-1);
fun(array,i+1,high);
}
void fun1(int[] array,int low,int high){
if (low > high){
return;
}
int key = array[low];
int i = low;
int j = high;
while (i < j){
while (i < j && array[j] >= key){
j--;
}
while (i<j && array[i] <= key){
i++;
}
if (i < j){
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
array[low] = array[i];
array[i] = key;
fun1(array,low,i-1);
fun1(array,i+1,high);
}
2.4 归并排序
思路: 假设待排序表含有 n 个记录,则可以看做是 n 个有序的子表,每个子表长度为 1 ,然后两两合并,得到 n / 2 个长度为 1 或 2 的有序表;再两两合并,直到合并成一个长度为 n 的有序表,这种排序方法称为 2-路归并排序
时间复杂度
- 平均情况:每一趟归并的时间复杂度为O(n),共进行 logn 趟归并,故算法的时间复杂度为 O(nlogn)
空间复杂度: 合并的过程中,辅助空间要占用 n 个单元,故时间复杂度为 O(n)
稳定性: 由于合并操作不会改变相同关键字记录的相对次序,所以2-路归并算法是稳定的排序方法
代码实现:
public void (int[] array,int low,int high){
if (low >= high){
return;
}
int mid = low + (high - low) / 2;
fun(array,low,mid);
fun(array,mid+1,high);
merge(array,low,mid,high);
}
public void merge(int[] array,int low,int mid,int high){
int temp = new int[high-low+1];
int i = low;
int j = mid+1;
int k = 0;
while (i <= mid && j<= high ){
if (array[i] < array[j]){
temp[k++] = array[i++];
}else{
temp[k++] = array[j++];
}
}
while(i<=mid){
temp[k++] = array[i++];
}
while(j <= high){
temp[k++] = array[j++];
}
for(int l =0;l<temp.length;l++){
array[low+l] = temp[l];
}
}
2.5 基数排序
思路:基数排序是一种很特殊的排序方法,他不是基于比较进行排序的,而是采用多关键字排序的思想,借助分配和收集两种操作对单逻辑关键字进行排序。基数排序又分为最高位优先(MSD)和最低位优先(LSD)排序
时间复杂度:O (nlog(r)m),其中 r 为所采取的基数,而 m 为堆数
空间复杂度:
稳定性:稳定的
代码实现:基数排序