常见排序算法的整理
冒泡排序
原理
每次比较相邻的两个数,如果顺序错误,就交换
最好时间复杂度:,数组原本有序
最坏时间复杂度:,数组倒序
平均时间复杂度:
空间复杂度:
稳定性:稳定
static int[] BubbleSort(int[] a) {
for (int i = 0; i < a.length; i++) {
boolean flag = true;
for (int j = 0; j < a.length - i-1; j++) {
if (a[j] > a[j + 1]) {
int[] swap = swap(a[j], a[j + 1]);
a[j] = swap[0];
a[j + 1] = swap[1];
flag = false;
}
}
if (flag) return a;
}
return a;
}
选择排序
原理
每次遍历数组,找到最小的,然后排到数组开头,依次进行n遍
最好时间复杂度:
最坏时间复杂度:
平均时间复杂度:
空间复杂度:
稳定性:不稳定
static int[] Selectsort(int[] a) {
for (int i = 0; i < a.length; i++) {
int tmpId = i;
for (int j = i + 1; j < a.length; j++) {
if (a[j] < a[tmpId]) tmpId = j;
}
int[] swap = swap(a[i], a[tmpId]);
a[i] = swap[0];
a[tmpId] = swap[1];
}
return a;
}
直接插入排序
原理
它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入
最好时间复杂度: 数组有序
最坏时间复杂度: 数组倒序
平均时间复杂度:
空间复杂度:
稳定性:稳定
static int[] insertionSort(int[] a) {
for (int i = 1; i < a.length; i++) {
int preIndex = i - 1;
int current = a[i];
while (preIndex >= 0 && a[preIndex] > current) {
a[preIndex + 1] = a[preIndex];
preIndex--;
}
a[preIndex + 1] = current;
}
return a;
}
希尔排序
原理
原则上来说,排序的目的就是消除逆序对,对于冒泡,选择这些 的算法来说,本质上每次比较最多只会消除1对逆序对,所以更快的排序方法本质上是交换相隔比较远的元素,使得一次交换能消除一个以上的逆序。
回到希尔排序,每次选择一个step,可以理解为以step个元素为一行,也就是有step列。每次就对每一列进行插入排序。然后缩小step,到step=1的时候就变成了普通的插入排序,但是这个时候数据已经基本有序了,只需交换较少的次数即可完成排序。
最好时间复杂度: 数组有序
最坏时间复杂度:取决于step
平均时间复杂度:取决于step
空间复杂度:
稳定性:不稳定
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GNoiymAG-1585122534553)(https://i.loli.net/2020/03/25/FN4DkKgU63ocs8Z.png)]
数据取自维基百科希尔排序
希尔排序的时间复杂度的下界是 。希尔排序对中等大小规模的数组表现良好,在最坏情况下和平均情况下执行效率差不多。
static int[] shellSort(int[] a) {
int length = a.length;
int temp;
for (int step = length / 2; step >= 1; step /= 2) {
for (int i = step; i < length; i++) {
temp = a[i];
int j = i - step;
while (j >= 0 && a[j] > temp) {
a[j + step] = a[j];
j -= step;
}
a[j + step] = temp;
}
}
return a;
}
归并排序
原理
采用分治的思想,将已有序的子序列合并,得到完全有序的序列
最好时间复杂度:
最坏时间复杂度:
平均时间复杂度:
空间复杂度:
稳定性:稳定
static void mergeSortRecursive(int[] a, int[] result, int l, int r) {
if (l >= r) return;
int mid = (l + r) / 2;
int st1 = l;
int st2 = mid + 1;
mergeSortRecursive(a, result, st1, mid);
mergeSortRecursive(a, result, st2, r);
int index = l;
while (st1 <= mid && st2 <= r) {
if (a[st1] < a[st2]) {
result[index++] = a[st1++];
} else {
result[index++] = a[st2++];
}
}
while (st1 <= mid) {
result[index++] = a[st1++];
}
while (st2 <= r) {
result[index++] = a[st2++];
}
for (int i = l; i <= r; i++) a[i] = result[i];
}
static int[] mergeSort(int[] a) {
int len = a.length;
int[] result = new int[len];
mergeSortRecursive(a, result, 0, len - 1);
return a;
}
快速排序
原理
通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
优化
- 空间的优化,朴素的快排的可以像归并一样申请空间存放比基准值大的值和存放比基准值小的元素,那么这里的空间复杂度就是的,但是可以使用原地分割的技术,大概就是指把基准值暂时移到数组的最后,然后维护一个index,表示比基准小的元素有几个,那么就将比基准值小的元素往前移,最后再把基准值移回来即可。这个的空间复杂度可以优化到,主要依赖于递归的深度。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eyl4ARCm-1585122534556)(https://i.loli.net/2020/03/25/yaWhTNqE7v3xL9m.png)]
-
对于选择基准值的优化。
- 固定位置基准值
- 随机选择基准值
- 三数取中选择基准值
对于固定选择,在极端数据下很容易退化成
对于随机选择,减少了极端情况的发生,一般不会退化很严重,当然有极小的概率会有问题。
对于三数取中,意思是选择一个数组中,最左边,中间,最右边的三个数,选择大小为中间的值作为基准值。相比于随机选择,最大好处在于处理升序数组效率比较高。
-
重复数据的优化
对于数组中出现大量重复数据的,可以将数组分成三段,而不是两端。
意思是分成小与基准值,等于基准值,大于基准值三类。
这样子在大量重复数据下的性能会比较好。
-
小范围的数据优化
对于很小和部分有序的数组,快排不如插排好。当待排序序列的长度分割到一定大小后,继续分割的效率比插入排序要差,此时可以使用插排而不是快排
最好时间复杂度:
最坏时间复杂度: 根据基准值的确定 ,每次左右分布极其不均衡
平均时间复杂度:
空间复杂度:取决于栈的深度,也就是之间
稳定性:不稳定
//这里就选择使用最基础的固定基准值
static void quickSortRecursive(int[] a, int l, int r) {
if (l >= r) return;
int val = a[l];
int i = l, j = r;
while (i < j) {
while (i < j && a[j] >= val) j--;
if (i < j) a[i++] = a[j];
while (i < j && a[i] < val) i++;
if (i < j) a[j--] = a[i];
}
a[i] = val;
quickSortRecursive(a, l, i - 1);
quickSortRecursive(a, i + 1, r);
}
static int[] quickSort(int[] a) {
quickSortRecursive(a, 0, a.length - 1);
return a;
}
堆排序
原理
堆这种数据结构,他的任意一个非叶节点的值都会比他的两个子节点的值大。
所以我们只需要构建出大根堆,每次移除大根堆顶,就能确定一个最大值,将这个值放在最后,然后只需要再次调整堆结构,就又能获得一个值。
最好时间复杂度:
最坏时间复杂度:
平均时间复杂度:
空间复杂度:
稳定性:不稳定
static void swap(int[] a, int i, int j) {
int tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}
static int[] heapSort(int[] a) {
int len = a.length;
for (int i = len / 2 - 1; i >= 0; i--) {
adjustHeap(i, a, len);
}
for (int i = len - 1; i >= 0; i--) {
swap(a, i, 0);
adjustHeap(0, a, i);
}
return a;
}
static void adjustHeap(int i, int[] a, int len) {
int l = 2 * i + 1;
int r = 2 * i + 2;
int largest = i;
if (l < len && a[l] > a[largest]) largest = l;
if (r < len && a[r] > a[largest]) largest = r;
if (largest == i) return;
swap(a, largest, i);
adjustHeap(largest, a, len);
}
计数排序
原理
将输入的数据值转化为键存储在额外开辟的数组空间中,需要确定最大值和最小值
注:n是数组长度,k是max-min的大小
最好时间复杂度:
最坏时间复杂度:
平均时间复杂度:
空间复杂度:
稳定性:稳定
static int[] countSort(int[] a) {
int b[] = new int[a.length];
int max = a[0];
int min = a[0];
for (int i = 1; i < a.length; i++) {
max = Math.max(max, a[i]);
min = Math.min(min, a[i]);
}
int k = max - min + 1;
int[] bucket = new int[k];
for (int i = 0; i < a.length; ++i) {
bucket[a[i] - min]++;
}
for (int i = 1; i < bucket.length; ++i) {
bucket[i] = bucket[i] + bucket[i - 1];
}
//保证稳定性
for (int i = a.length - 1; i >= 0; --i) {
b[--bucket[a[i] - min]] = a[i];
}
return b;
}
桶排序
原理
将数组分到有限数量的桶里。每个桶再个别排序。上述的基数排序可以理解为特殊的桶排。
使用场景
一般只适用于数据分布均匀的场景。
时间复杂度和空间复杂度取决于桶的设置。
稳定性:稳定
基数排序
原理
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位
也可以理解为每次有10个桶,然后把数字放到桶里面。
时间复杂度:,但是带有不确定的常数(即位数)
空间复杂度:,n是数组长度,k是基数
稳定性:稳定