排序
排序的分类
按是否占用额外内存(原地排序):
out-place | 归并排序、计数排序、基数排序、桶排序 |
in-place | 冒泡排序、插入排序、选择排序、快排、堆排序 |
按是否为稳定排序分类:
stable | 冒泡排序、插入排序、归并排序、计数排序、基数排序、桶排序 |
unstable | 选择排序(5 8 5 2 9)、快排、堆排序 |
PS:如何修改不稳定的排序算法使之稳定?
对每个输入的元素加入一个索引index,排序完之后相同的元素按照index再次排序即可。
按时间复杂度:
算法 | 时间复杂度 | 是否基于比较 |
---|
冒泡、插入、选择 | O(n^2) | 是 |
快排、归并 | O(nlogn) | 是 |
计数、基数、桶 | O(n) | 否 |
总结:

冒泡排序
特点 | stable sort、in-place sort |
最优复杂度 | 数组已经排好序时:O(n) |
最差时间复杂度 | 数组逆序排序时:O(n^2) |
平均时间复杂度 | O(n^2) |
插入排序
特点 | stable sort、in-place sort |
最优复杂度 | 数组已经排好序时:O(n) |
最差时间复杂度 | 数组逆序排序时:O(n^2) |
适合数组类型 | 比较适合“少量元素数组” 插入排序的排序速度是数组中逆序对的个数,逆序越少,插入排序越快 |
插入排序的过程:
数组的左边有序,右边无序,依次从右边选取数字插入到左边合适的位置。
排序初始时,数组左边为空
Q1.快排是否一定比插入排序快?(数据不随机)
不是的,当数组是顺序排好时,使用快排的时间复杂度是O(n^2),而插入排序的时间复杂度是O(n)
Q2.冒泡排序和插入排序哪个快?
插入排序快。插入排序的速度是逆序对的个数,而冒泡排序中,执行“交换”过程的次数就是逆序对的个数,因此冒泡运行的时间至少是逆序对的个数。
选择排序
最优/最差时间复杂度 | O(n^2) |
平均时间复杂度 | O(n^2) |
空间复杂度 | O(1) |
是否稳定 | 不一定 |
快速排序
性质 | in-place sort |
最优时间复杂度 | O(nlogn) |
最差时间复杂度 | 数组已经排序时:O(n^2) |
特点 | 分治的思想 |
代码:
void QuickSort(int a[], int l, int r)
{
if (l >= r) return;
int i = l, j = r, key = a[i];
while (i < j)
{
while (i < j && key <= a[j])
j--;
a[i] = a[j];
while (i < j && key >= a[i])
i++;
a[j] = a[i];
}
a[i] = key;
QuickSort(a, l, i - 1);
QuickSort(a, i + 1, r);
}
归并排序
性质 | out-place sort |
最优/最差时间复杂度 | O(nlogn) |
空间复杂度 | O(n) |
特点 | 分治的思想解决问题 |
代码(C):
void merge(int a[], int l,int r,int mid)
{
int len = r - l + 1;
int *tmp = new int[len];
int k = 0;
int i = l;
int j = mid + 1;
while (i <= mid && j <= r)
tmp[k++] = a[i] < a[j] ? a[i++] : a[j++];
while (i <= mid)
tmp[k++] = a[i++];
while (j <= r)
tmp[k++] = a[j++];
for (k = 0; k < len; k++)
a[l++] = tmp[k];
}
void merge_sort(int a[], int l, int r)
{
if (l == r) return;
int mid = (l + r) / 2;
merge_sort(a, l, mid);
merge_sort(a, mid + 1, r);
merge(a, l, r, mid);
}
代码(JAVA):
public void mergeSort(int nums[], int left, int right) {
if(left == right) return;
int mid = (left+right)/2;
mergeSort(nums, left, mid);
mergeSort(nums, mid+1, right);
merge(nums, left, right, mid);
}
public void merge(int[] nums, int left, int right, int mid) {
int len=right-left+1;
int[] tmps=new int[len];
int k=0,i=left,j=mid+1;
while(i<=mid&&j<=right)
tmps[k++]=nums[i]<nums[j]?nums[i++]:nums[j++];
while(i<=mid)
tmps[k++]=nums[i++];
while(j<=right)
tmps[k++]=nums[j++];
for(int tmp : tmps) {
nums[left++] = tmp;
}
}
Q1.归并排序的缺点是什么?
需要额外的空间(额外的多少空间)
Q2.归并排序和快速排序哪个更快?
快排更快,因为虽然渐近复杂度一样,但是归并排序的系数比快排的大
Q3.归并排序如何改进?
在数组长度为k时使用插入排序(插入排序适合对小数组排序)
其中k=O(logn),此时算法的复杂度为O(nlogn)
堆排序
性质 | unstable sort、in-place sort |
最优/最差时间复杂度 | O(nlogn) |
计数排序(comparison-counting sort)
性质 | stable-sort,out-place sort |
最优/最差时间复杂度 | O(n^2) |
计数排序的过程是,从第一个数开始,记录比它更小的数有多少,这个数字就是它排序完成的数组坐标。
计数排序的延伸还有一个分布计数排序(distribution counting) 。
考虑数组:
13 11 12 13 12 12
已知他们来自集合{11,12,13}。在这种情况下使用分布计数排序效果较好:
首先统计这些数字出现的频率,然后算出分布值:
排序过程为:
第一个出现的数字是13,排在第5位
分布值更新为:
排序数组:
第2个出现的数字是11,排在第0位
分布值更新为:
排序数组:
第3个出现的数字是12,排在第3位
分布值更新为:
排序数组:
第4个出现的数字是13,排在第4位
分布值更新为:
排序数组:
第5个出现的数字是12,排在第2位
分布值更新为:
排序数组:
最后一个出现的数字是12,排在第1位
分布值更新为:
排序数组:
桶排序
性质 | stable sort、out-place sort |
最优/最差时间复杂度 | |
Java实现
List<Integer>[] bucket = new List[bucket_length];
for(int i=0;i<bucket_length;i++)
bucket[i] = new ArrayList<>();