概述
1、排序的定义
对一序列对象根据某个关键字进行排序。
2、术语
稳定性:如果 a 等于 b,a 排在 b 的前面,排序后 a 一定仍然在 b 的前面,那么当前排序算法是稳定算法,否则就不是稳定算法。
原地算法:不依赖额外的资源或者依赖少数的额外资源,仅依靠输出来覆盖输入,空间复杂度为 O(1)的都可以认为是原地算法。
3、分类
4、复杂度
名称 | 时间复杂度 | 额外空间复杂度 | 原地算法 | 稳定性 | ||
最好 | 最坏 | 平均 | ||||
冒泡排序 | O(n) | O(n^2) | O(n^2) | O(1) | 是 | 是 |
选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 是 | 否 |
插入排序 | O(n) | O(n^2) | O(n^2) | O(1) | 是 | 是 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 否 | 是 |
快速排序 | O(nlogn) | O(n^2) | O(nlogn) | O(logn) | 是 | 否 |
希尔排序 | O(n) | O(n^4/3~n^2) | 取决于步长序列 | O(1) | 是 | 否 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 是 | 否 |
计数排序 | O(n+k) | O(n+k) | O(n+k) | O(n+k) | 否 | 是 |
基数排序 | O(d*(n+k)) | O(d*(n+k)) | O(d*(n+k)) | O(n+k) | 否 | 是 |
桶排序 | O(n+k) | O(n+k) | O(n+k) | O(n+m) | 否 | 是 |
一、冒泡排序
1、执行流程
1、从头开始比较每一个相邻元素,如果第一个比第二个大,就交换它们的位置
执行完一轮后,最末尾那个元素就是最大的元素
2、忽略步骤 1 中曾经找到的最大元素,重复执行步骤 1,直到全部元素有序
2、实现代码
下面是冒泡排序的简单实现:
protected void sort() {
// 从后往前遍历,end指针后面的已排序,end前面的未排序
for (int end = array.length - 1; end > 0; end--) {
// 比较相邻两个元素
for (int begin = 0; begin < end; begin++) {
if (cmp(begin, begin + 1) > 0) {
swap(begin, begin + 1);
}
}
}
}
可以添加一个记录最后一次交换位置的索引,并赋值给 end,end 下次从这个位置开始往前遍历。这样可以提高效率,当数据有序时,遍历一遍外层循环就退出了,最好情况是 O(n)。
protected void sort() {
// 从后往前遍历,end指针后面的已排序,end前面的未排序
for (int end = array.length - 1; end > 0; end--) {
int sortedIndex = 0;
// 比较相邻两个元素
for (int begin = 0; begin < end; begin++) {
if (cmp(begin, begin + 1) > 0) {
swap(begin, begin + 1);
sortedIndex = begin + 1;
}
}
end = sortedIndex;
}
}
二、快速排序
1、执行流程
1、从序列中选择一个轴点元素(pivot),假设每次选择 0 位置的元素做为轴点元素
2、利用 pivot 将序列分割成 2 个子序列,将小于 pivot 的元素放在 pivot 前面,将大于或等于 pivot 的元素放在 pivot 后面
3、对子序列进行 1,2 循环操作,直到不能再分割(子序列只剩下一个元素)
2、实现代码
public class QuickSort<T extends Comparable<T>> extends Sort<T> {
@Override
protected void sort() {
// [begin,end) 左闭右开区间
sort(0, array.length);
}
private void sort(int begin, int end) {
if (end - begin < 2) return;
int mid = pivotIndex(begin, end);
sort(begin, mid);
sort(mid + 1, end);
}
private int pivotIndex(int begin, int end) {
T pivot = array[begin];
// 将左闭右开的end元素指向数组最后一个元素
end--;
while (begin < end) {
while (begin < end) {
if (cmp(pivot, array[end]) < 0) { // 右边元素 > 轴点元素
end--;
} else { // 右边元素 <= 轴点元素
array[begin++] = array[end];
break;
}
}
while (begin < end) {
if (cmp(pivot, array[begin]) > 0) { // 左边元素 < 轴点元素
begin++;
} else { // 左边元素 >= 轴点元素
array[end--] = array[begin];
break;
}
}
}
array[begin] = pivot;
return begin;
}
}
如果轴点元素一直是最小或最大元素,那么快速排序的时间复杂度为 O(n^2)。
可以将默认选择第一个元素作为轴点元素改成,随机选择一个元素作为轴点元素,然后与第一个元素交换位置,这样只需要一个小改动即可:
// 随机选择一个元素跟begin位置进行交换
swap(begin, begin + (int)(Math.random() * (end - begin)));
T pivot = array[begin];
三、插入排序
1、执行流程
1、在执行过程中,插入排序会将序列分为2部分,头部是已经排好序的,尾部是待排序的
2、从头部开始扫描每一个元素,每当扫描到一个元素,就将它插入到头部合适的位置,使得头部数据依然保持有序
插入排序的时间复杂度与逆序对的数量成正比
2、实现代码
简单实现:
private void sort1() {
for (int begin = 0; begin < array.length; begin++) {
int cur = begin;
while (cur > 0 && cmp(cur, cur - 1) < 0) {
swap(cur, cur - 1);
cur--;
}
}
}
找到合适位置后再进行替换:
private void sort2() {
for (int begin = 1; begin < array.length; begin++) {
int cur = begin;
// 当前元素
T v = array[cur];
while (cur > 0 && cmp(v, array[cur - 1]) < 0) {
// 前移
array[cur] = array[cur - 1];
cur--;
}
// 找到合适位置后,将当前元素赋值回去
array[cur] = v;
}
}
四、希尔排序
希尔排序把序列看作是一个矩阵,分成 m 列 ,逐列进行排序
1、实现步骤
1、m 从某个整数逐渐减为1
2、当 m 为1时,整个序列将完全有序
矩阵的列数取决于步长序列,希尔本人给出的步长序列是 n / 2^k
每一列通过插入排序算法排序,从多列编程一列的过程中,逆序对逐渐减少
2、实现代码
protected void sort() {
List<Integer> stepSequence = shellStepSequence();
for (Integer step : stepSequence) {
sort(step);
}
}
/**
* 分成step列进行排序
*/
private void sort(int step) {
// col : 第几列
for (int col = 0; col < step; col++) { // 对第col列进行排序
// col、col+step、col+2*step、col+3*step
for (int begin = col + step; begin < array.length; begin += step) {
int cur = begin;
while (cur > col && cmp(cur, cur - step) < 0) {
swap(cur, cur - step);
cur -= step;
}
}
}
}
// 分成 n/2^k 列
private List<Integer> shellStepSequence() {
List<Integer> stepSequence = new ArrayList<>();
int step = array.length;
while ((step >>= 1) > 0) {
stepSequence.add(step);
}
return stepSequence;
}
五,选择排序
1、实现步骤
1、从序列中找出最大的那个元素,然后与最末尾的元素交换位置,执行完一轮后,最末尾的那个元素就是最大的元素
2、忽略步骤1中刚刚最大的元素,重复执行步骤1
2、实现代码
protected void sort() {
for (int end = array.length - 1; end > 0; end--) {
int max = 0;
for (int begin = 1; begin <= end; begin++) {
if (cmp(max, begin) < 0) {
max = begin;
}
}
swap(max, end);
}
}
六、堆排序
选择排序的思路是找最大元素,然后与末尾元素交换。找最大元素可以使用堆,所以选择排序可以优化成使用堆排序。
1、实现步骤
1、对堆序列进行原地建堆
2、交换栈顶元素与尾元素,堆的元素数量减1,对0位置进行1次下溢操作
3、重复执行步骤2
2、实现代码
protected void sort() {
// 原地建堆
heapSize = array.length;
for (int i = (heapSize >> 1) - 1; i >= 0; i--) {
siftDown(i);
}
while (heapSize > 1) {
// 交换堆顶元素和尾部元素
swap(0, --heapSize);
// 对0位置进行siftDown(恢复堆的性质)
siftDown(0);
}
}
private void siftDown(int index) {
T element = array[index];
int half = heapSize >> 1;
while (index < half) { // index必须是非叶子节点
// 默认是左边跟父节点比
int childIndex = (index << 1) + 1;
T child = array[childIndex];
int rightIndex = childIndex + 1;
// 右子节点比左子节点大
if (rightIndex < heapSize &&
cmp(array[rightIndex], child) > 0) {
child = array[childIndex = rightIndex];
}
// 大于等于子节点
if (cmp(element, child) >= 0) break;
array[index] = child;
index = childIndex;
}
array[index] = element;
}
七、归并排序
1、实现步骤
1、不断地将当前序列平均分割成2个序列,直到不能再分割(序列中只有一个元素)
2、不断地将2个序列合并成一个有序序列,最终只剩下1个有序序列
需要 merge 的 2 组序列存在同一个数组中,并且是挨在一起的,为了更好的完成 merge 操作,最好将其中1组序列备份出来,比如左边序列 leftArray ([begin,end))。
2、实现代码
@Override
protected void sort() {
leftArray = (T[]) new Comparable[array.length >> 1];
sort(0, array.length);
}
/**
* 对 [begin, end) 范围的数据进行归并排序
*/
private void sort(int begin, int end) {
if (end - begin < 2) return;
int mid = (begin + end) >> 1;
sort(begin, mid);
sort(mid, end);
merge(begin, mid, end);
}
/**
* 将 [begin, mid) 和 [mid, end) 范围的序列合并成一个有序序列
*/
private void merge(int begin, int mid, int end) {
int li = 0, le = mid - begin;
int ri = mid, re = end;
int ai = begin;
// 备份左边数组
for (int i = li; i < le; i++) {
leftArray[i] = array[begin + i];
}
// 如果左边还没有结束
while (li < le) {
if (ri < re && cmp(array[ri], leftArray[li]) < 0) {
array[ai++] = array[ri++];
} else {
array[ai++] = leftArray[li++];
}
}
}
3、复杂度分析
T(n) = 2 * T( n / 2 )+ O(n) 可以推出归并排序的时间复杂度为 O(nlogn)。下面是常见的递推公式:
递推式 | 复杂度 |
T(n) = T( n / 2 )+ O(1) | O(logn) |
T(n) =T( n -1 )+ O(1) | O(n) |
T(n) = T( n / 2 )+ O(n) | O(n) |
T(n) = 2 * T( n / 2 )+ O(1) | O(n) |
T(n) = 2 * T( n / 2 )+ O(n) | O(nlogn) |
T(n) = T( n -1 )+ O(n) | O(n^2) |
T(n) = 2 * T( n-1 )+ O(1) | O(2^n) |
T(n) = 2 * T( n -1 )+ O(n) | O(2^n) |
八、非比较排序
前面的排序都是基于比较的排序,时间复杂度目前最低为 O(nlogn)。
计数排序,桶排序,基数排序都不是基于比较的排序。他们是典型的用空间换时间 ,在某些时候,平均时间复杂度比 O(nlogn) 更低。
1、计数排序
统计每个整数在序列中出现的次数,进而推导出每个整数在有序序列中的索引。
实现步骤:
- 找出待排序的数组中最大和最小的元素;
- 统计数组中每个值为 i 的元素出现的次数,存入数组 C 的第 i 项;
- 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
- 反向填充目标数组:将每个元素 i 放在新数组的第 C(i) 项,每放一个元素就将 C(i) 减去1。
上面的计数排序一般实现,有兴趣的可以自行去优化。
2、基数排序
基数排序非常适合于整数排序,执行流程如下:
- 依次对个位数,十位数,百位数,千位数,万位数...进行排序
- 十位数,百位数,千位数,万位数取值范围都是固定的 0~9,可以使用计数排序进行排序
3、桶排序
执行流程
- 创建一定数量的痛
- 按照一定的规则(不同类型的数据,规则不同),将序列中的元素均匀分配到对应的桶
- 分别对每个桶进行单独培训
- 将所有非空桶的元素合并成有序序列
例如:
0.34 | 0.47 | 0.29 | 0.84 | 0.45 | 0.38 | 0.35 | 0.76 |
元素在桶中的索引:
可以设定一个规则:元素值 * 元素数量
0 | |||
1 | |||
2 | 0.34 | 0.29 | 0.35 |
3 | 0.47 | 0.45 | 0.38 |
4 | |||
5 | |||
6 | 0.84 | 0.76 | |
7 |
然后对每个桶排序,再合并每个桶完成排序