十大排序算法
1. 冒泡排序
1.1 概括
为什么叫冒泡排序?每一次排序都将最大(最小)的元素交换到顶端,就像是气泡上浮一样,所以称之为冒泡排序。
一句话概括:从前往后,两两比较,大者上浮,以此往复。
从图中也可以看出,从前往后两两比较,大者上浮,排在后面的几个数是排好序的,所以每次比较我们只需要比较前面未排好序的序列。
1.2 思路
1.2.1 基本思路
- 从左往右比较相邻的两个元素,如果第一个比第二个大,就交换他们。
- 依次对相邻的元素作重复以上工作,从开始一对到最后一对,最后一个元素就变成最大值了。
- 针对所有元素重复以上步骤,除了最后一个元素!
- 然后对未排好序的元素重复以上步骤,直到所有元素均完成排序。
1.2.2 优化思路
- 如果排序之前或者排序过程中, 出现右边所有元素均大于左边元素的情况。
- 此时数组有序,所有元素均不需要进行交换,那我们的比较就没有意义了。
- 所以可以设置一个标志flag,如果未发生交换,就说明排序已经完成,无需再比较,直接返回。
1.3 代码
/**
* 冒泡排序
* int[] a = { 1,5,7,6,2,4 };
* // 1 5 6 2 4 7
* // 1 5 2 4 6
* // 1 2 4 5
* // 1 2 4
* // 1 2 //此处可以解释为什么 i < len - 1
* 也可以说,正是因为两两比较,成对比较,所以只要比较到倒数第一对,即,倒数第二个元素就可以了。
*
* len = 6
* index: i 为外循环次数 j 为需比较交换的元素位置
* i: 01234
* j: 43210 ---> j <= len - i
*/
//冒泡排序
public int[] sortBubble(int[] a) {
int len = a.length;
for (int i = 0; i < len - 1; i++) {
for (int j = 0; j < len - i - 1; j++) {
if (a[j + 1] < a[j]) {
int tmp = a[j];
a[j] = a[j + 1];
a[j + 1] = tmp;
}
}
}
return a;
}
// 优化后的冒泡排序
public int[] bubbleSort(int[] a) {
int len = a.length;
boolean flag = false;
for (int i = 0; i < len - 1; i++) {
for (int j = 0; j < len - i - 1; j++) {
if (a[j + 1] < a[j]) {
flag = true;
int tmp = a[j];
a[j] = a[j + 1];
a[j + 1] = tmp;
}
}
if (!flag) {
return a;
}
}
return a;
}
1.4 算法分析
- 时间复杂度:O(n2)
- 空间复杂度:O(1)
- 稳定排序:如果 a 原本在 b 的前面,且 a == b,排序之后 a 仍然在 b 的前面,则为稳定排序。
为什么是稳定排序呢?因为我们将两两元素作比较时,a[j + 1] < a[j],若a==b,也不会交换位置,始终保持a 在 b 的前面。 - 原地排序:原地排序就是指在排序过程中不申请多余的存储空间,只利用原来存储待排数据的存储空间进行比较和交换的数据排序。
2. 选择排序
2.1 概括
在第 i 次遍历中选择最小的元素并与数组前面的第 i 个元素进行交换。
何谓选择?就是选择最小的元素。
2.2 思路
首先,找到数组中最小的那个元素,并将它和数组中第一个元素交换位置(如果第一个元素就是最小元素,则和自己交换),然后,在剩下的元素中找到最小的元素,与数组中第二个元素交换位置,如此往复,直到排序完成。
也就是每次都先将前面较小的数排好序,在后面的序列中找到一个最小的数,把小的数都调换到前面来。
2.2.1 关于交换操作的优化
当在剩下的元素中找到最小值时,如果找到了就进行一次交换,是浪费时间的,因为当前找到的最小值很有可能是局部最小值,每找到一个就交换的话,无疑会增加交换次数。正确的做法应该是当找到剩下元素序列的全局最小值时,才进行交换,每一次遍历只交换一次就够了;我们可以只记录最小值的坐标以达到一次遍历一次交换的目的。
2.3 代码
/**
* 选择排序,我的做法
*/
public int[] selectSort1(int[] a) {
int len = a.length;
for (int i = 0; i < len; i++) {
//i < len - 1 会更好
//因为当i=len-1时,下面j=len,已经进不去循环了,没有意义
int mini = a[i];
for (int j = i + 1; j < len; j++) {
if (a[j] < mini) {
int tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}
}
}
return a;
}
优化后的选择排序
/**
* 优化后的选择排序
*/
public int[] selectSort2(int[] a) {
int len = a.length;
for (int i = 0; i < len - 1; i++) {
// 改为len - 1
// 因为遍历到倒数第二个元素时,就可以将它和最后一个比较,决定是否交换
int mini = i;// 记录位置,而不是变量
for (int j = i + 1; j < len; j++) {
if (a[j] < a[mini]) {
mini = j;
}
}
// 将交换操作放到外面,好处:
// 如果放到里面,那么只要找到一个比mini小的元素就要进行交换
// 而放在外面,一次遍历后,找到最小值才会进行交换
int tmp = a[i];
a[i] = a[mini];
a[mini] = tmp;
}
return a;
}
2.4 算法分析
- 时间复杂度:O(n2)
- 空间复杂度:O(1)
- 非稳定排序: 如果 a 原本在 b 的前面,且 a == b,排序之后 a 可能不在 b 的前面,则为非稳定排序。
为什么是非稳定性排序?因为假如存在数组为 4 4 5 2 1,设第一个4 为 4a, 第二个4 为 4b。
那么第一次遍历时,1 为最小值,1 与4a 进行交换,那么 4a就换到 4b后面去了,也就构成了非稳定排序。 - 原地排序: 原地排序就是指在排序过程中不申请多余的存储空间,只利用原来存储待排数据的存储空间进行比较和交换的数据排序。
3. 插入排序
3.1 概括
将一个记录插入到已经排好序的有序表中。
3.2 思路
3.2.1 概括
对于少量元素是一个有效的算法。外层循环针对除第一个元素之外的所有元素,内层循环对当前元素前面的有序表进行待插入位置的查找。
3.2.2 抽象理解
插入排序的工作方式像许多人排序一手扑克牌。开始时,我们的左手为空并且桌子上的牌面向下。然后,我们每次从桌子上拿走一张牌并将它插入左手中正确的位置。为了找到一张牌的正确位置,我们从右到左将它与已在手中的每张牌进行比较。从右往左找到第一张比自己小的牌,将牌插在它的右边。拿在左手上的牌总是排序好的。
3.2.3 具体思路:
1、从数组第2个元素开始抽取元素。
2、把它与左边第一个元素比较,如果左边第一个元素比它大,则继续与左边第二个元素比较下去,直到遇到不比它大的元素,然后插到这个元素的右边。
3、继续选取第3,4,…n个元素,重复步骤 2 ,选择适当的位置插入。
2.3 代码
public int[] insertSort(int[] a) {
if (a == null || a.length < 2) {
return a;
}
for (int i = 1; i < a.length; i++) {
int tmp = a[i];// 先保存下来,防止插入时,元素移动后被覆盖
int k = i - 1;
//用k去寻找左侧比自己小的数,小于等于
while (k >= 0 && a[k] > tmp) {
k--;
}
//找到之后将自己放在此数右侧以构成有序序列
//即,将此数右边的直到 i 的所有元素右挪
for (int j = i; j > k + 1; j--) {
a[j] = a[j - 1];
}
a[k + 1] = tmp;//插入到这个比自己小的数的右边,所以是k+1
}
return a;
}
改进的插入排序
//改进的插入排序
public int[] insertSort2(int[] a) {
if (a == null || a.length < 2) {
return a;
}
for (int i = 1; i < a.length; i++) {
int tmp = a[i];// 先保存下来,防止插入时,元素移动后被覆盖
int k = i - 1;
//改进为,在寻找比自己小的数的这个过程中,就进行挪位操作
//也即,比我大,就去我后面,覆盖我的位置没关系,反正我存在tmp里了
while (k >= 0 && a[k] > tmp) {
a[k + 1] = a[k];
k--;
}
a[k + 1] = tmp;//最后将自己放在指定位置
}
return a;
}
2.4 算法分析
- 时间复杂度:O(n2)
- 空间复杂度:O(1)
- 稳定排序:如果 a 原本在 b 的前面,且 a == b,排序之后 a 仍然在 b 的前面,则为稳定排序。
为什么是稳定排序呢?因为假如存在数组为 5 4 4 2 1,设第一个4 为 4a, 第二个4 为 4b。
4a 会插入到 5 的前面,此时4b 再去插入的时候,去找第一个小于等于自己的数,是4a,那么4b就插入在4a的右边,顺序不变。
其实,这里还是有点不理解。程序中我们把while (k >= 0 && a[k] > tmp) 改为while (k >= 0 && a[k] >= tmp)之后,排序结果不变,但是这样4b 就会插入到 4a 的左边,导致顺序变化,构成不稳定排序。???怎么说呢,a[k] >= tmp就是将和自己相等的那个数也挪到自己的右边,我插入到你前面,这不是多此一举吗,多挪了一次,浪费时间,不符合我们设计算法的初衷。 - 原地排序: 原地排序就是指在排序过程中不申请多余的存储空间,只利用原来存储待排数据的存储空间进行比较和交换的数据排序。
4. 希尔排序
希尔排序可以说是插入排序的一种变种。无论是插入排序还是冒泡排序,如果数组的最大值刚好是在第一位,要将它挪到正确的位置就需要 n - 1 次移动。也就是说,原数组的一个元素如果距离它正确的位置很远的话,则需要与相邻元素交换很多次才能到达正确的位置,这样是相对比较花时间了。
4.1 概括
希尔排序就是为了加快速度,简单地改进了插入排序,交换不相邻的元素以对数组的局部进行排序。
也就是说,希尔排序减少了交换次数。
4.2 思路
希尔排序的思想是采用插入排序的方法,先让数组中任意间隔为 h 的元素有序,刚开始 h 的大小可以是 h = n / 2,接着让 h = n / 4,让 h 一直缩小,当 h = 1 时,也就是此时数组中任意间隔为1的元素有序,此时的数组就是有序的了。
简单来讲,希尔排序先将数组按照一定间隔分组,对每一个组进行插入排序。然后缩小组间间隔,扩大单个组,再次进行插入排序,直到组内间隔为1,构成有序序列。
即,希尔排序以一种间隔的方式对局部进行排序,缩小数字离它正确位置的距离,减少交换次数。
需要注意的是,对各个分组进行插入的时候并不是先对一个组排序完了再来对另一个组排序,而是轮流对每个组进行排序。
4.3 代码
/**
* 希尔排序
*
* @param a
* @return
*/
public int[] shellSort(int[] a) {
if (a == null || a.length < 2) {
return a;
}
int n = a.length;
for (int h = n / 2; h > 0; h /= 2) {
// 对数组进行分组,间隔为h
for (int i = h; i < n; i++) {
// 对分组轮流进行直接插入排序
// h 为第一个分组的第二个元素,
// h + 1 为第二个分组的第二个元素,
// 第二个元素正是插入排序开始比较的起点
insertSort(a, h, i);
}
}
return a;
}
public void insertSort(int[] a, int h, int i) {
//这里的插入排序与普通插入排序的区别就在于,这里的间隔是h,而普通的间隔为1
int tmp = a[i];// 记录每一组的第二个元素
int k = i - h;//即,该组左边的第一个元素
// 用k去寻找左侧比自己小的数
while (k >= 0 && tmp < a[k]) {
k -= h;// 找本组内自己左边比自己小的元素,所以是k-=h
}
// 找到之后将自己放在此数右侧以构成有序序列
// 即,将此数右边的直到 i 的所有元素右挪
for (int j = i ; j > k + h; j-=h) {
a[j] = a[j - h];
}
a[k + h] = tmp;
}
public void insertSort2(int[] a, int h, int i) {
int tmp = a[i];// 记录每一组的第二个元素
int k = i - h;//即,该组左边的第一个元素
// 用k去寻找左侧比自己小的数
while (k >= 0 && tmp < a[k]) {
a[k + h] = a[k];
k -= h;// 找本组内自己左边比自己小的元素,所以是k-=h
}
a[k + h] = tmp;
}
4.4 算法分析
- 时间复杂度:O(nlogn)
- 空间复杂度:O(1)
- 非稳定排序
为什么非稳定?因为就如图中,如果将2后面的3也改为2,可是由于二者不在一个组内,组内进行插入排序时,后面那个2就排到了前面,导致顺序变换。 - 原地排序
5. 归并排序
5.1 概括
一句话来说,就是先将无序数组分割成子序列,对子序列排序后,再对子序列进行合并。
5.2 思路
5.2.1 分治的思想
分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。分的阶段就是将数组拆分的阶段,而治的阶段就是排序合并的阶段。
若将两个有序表合并成一个有序表,称为二路归并。
5.2.2 具体思路
归并排序对数组进行分割,直到每一组的元素数量为1 ,那么这个长度为1的数组肯定是有序的,之后将两个只有一个元素的数组合并为长度为2的数组,依次进行合并,直到合并完成排序
5.2.3 合并操作
假如有数组A,B,临时数组T;设置三个指针,分别指向A,B,T,的初始位置;依次比较A,B两个数组的元素,若A的元素小,则A的元素加入T,A指针++,否则B的元素加入T,B指针++,最后,如果有一方剩余,则直接加入T。这么做的依据是,A,B两个数组都是已经排好序的,对于归并排序而言,最初是两个长度为1的数组合并,所以这两个数组必然有序。
5.3 代码
5.3.1 递归
通过递归的方式将大的数组一直分割,直到数组的大小为 1,此时只有一个元素,那么该数组就是有序的了,之后再把两个数组大小为1的合并成一个大小为2的,再把两个大小为2的合并成4的 … 直到全部小的数组合并起来。
/**
* 归并排序
*/
public int[] mergeSort(int[] a, int left, int right) {
if (left == right) {
// left,right相等则当前数组中只有一个元素,不用再进行分割了
return a;
}
int mid = (left + right) / 2;// 数组分割为左半部分
a = mergeSort(a, left, mid);// 左半部分的分割,赋值给a,就是更新数组
a = mergeSort(a, mid + 1, right);// 右半部分的分割
merge(a, left, mid, right);// 进行合并并排序
return a;
}
public void merge(int[] a, int left, int mid, int right) {
// 对排好序的两个数组进行排序
int[] tmp = new int[right - left + 1];
int i = left;
int j = mid + 1;
int k = 0;
while (i <= mid && j <= right) {
// 较小者进入临时数组
if (a[i] < a[j]) {
tmp[k++] = a[i++];
} else {
tmp[k++] = a[j++];
}
}
// 如果一方有剩余则直接加入临时数组
// {6,202},{100,301},{8,38},{1}
// 主要是针对{1}这种情况
//{8,38},{1}二者合并时,{1}率先落位,剩下的{8,38}就直接加入数组
while (i <= mid) {
tmp[k++] = a[i++];
}
while (j <= right) {
tmp[k++] = a[j++];
}
// 将数组复制回原数组
for (i = 0; i < tmp.length; i++) {
a[left + i] = tmp[i];
}
}
5.3.2 非递归
也分为划分和合并,将数组划分为子数组,子数组的大小为1,2,4,8… ,首先对长度为1的数组进行两两合并,然后对每一种子数组依次进行两两合并。注意是两两合并,注意下标变化。
public int[] mergeSort2(int[] a) {
// 非递归的归并排序
int n = a.length;
for (int i = 1; i < n; i += i) {// 注意i += i
// i为子数组的长度
int left = 0;// 每种子数组第一个数组的最左边
int mid = left + i - 1;// 两个子数组中间的那个间隔位置
int right = mid + i;// 每种子数组第二个数组的最右边
while (right < n) {
// 合并函数与递归的一样
merge(a, left, mid, right);
left = right + 1;
mid = left + i - 1;
right = mid + i;
}
// 剩余的数组,不可能每个字数组的大小都刚好为 i
// {6,202},{100,301},{8,38},{1}
// 主要是针对{8,38},{1}二者的合并
if (left < n && mid < n) {
merge(a, left, mid, n - 1);
}
}
return a;
}
是不是觉得和希尔排序有相似的地方,希尔排序是分组后,对组内进行插入排序,缩小数字与正确位置的距离,减少交换次数;而归并排序的不同之处,在于归并排序一分到底,然后对两个组之间进行排序合并;一分到底、归并 这都是与希尔不同的地方。
5.4 算法分析
速度仅次于快速排序,为稳定排序算法,一般用于对总体无序,但是各子项相对有序的数列。
- 时间复杂度:O(nlogn)
- 空间复杂度:O(n)
- 稳定排序
为什么稳定? - 非原地排序
为什么非原地?
6. 快速排序
6.1 概括
设置一个枢轴,寻找枢轴的位置,使枢轴前面的数都比自己小,后面的数都比自己大。找到枢轴的位置,根据该位置将数组划分为子序列,再依次进行排序,即,利用递归。
6.2 思路
寻找一个枢轴位置的过程称为一次快速排序。一次快排的具体做法是:设置一个枢轴,先从右往左,寻找第一个比枢轴小的数,交换位置;再从左往右,寻找第一个比枢轴大的数,交换位置。重复操作,直到找到枢轴位置(low == high)。
优化:
交换位置的操作可以优化。
6.3 代码
public int[] quickSort(int[] a, int low, int high) {
if (low < high) {
int pivo = partition(a, low, high);
quickSort(a, low, pivo - 1);
quickSort(a, pivo + 1, high);
}
return a;
}
public int partition(int[] a, int low, int high) {
int pivo = a[low];// 设置枢轴
while (low < high) {
while (low < high && a[high] > pivo) {
// 从数组右边开始找
// 寻找一个比枢轴小的数,这样枢轴右边的数都比枢轴大
// 简单点说,只要比自己大的数全部略过
high--;
}
// 找到之后互换位置
a[low] = a[high];
while (low < high && a[low] < pivo) {
// 从左往右,寻找一个比枢轴大的数,这样枢轴右边的数都比枢轴小
// 简单点说,只要比自己小的数全部略过
low++;
}
a[high] = a[low];
}
a[low] = pivo;
return low;
}
6.4 算法分析
- 时间复杂度:O(nlogn)
- 空间复杂度:O(logn) 递归的深度。
- 非稳定排序 :
这个简单来说就是,交换的时候不能保证重复数字的顺序。加入存在一个数组,6 4a 3 2 5 4b
设 6 为枢轴,首先从后往前找比6小的数,交换,那么4b就交换到4a前面了,改变原来的顺序,所以为非稳定排序。 - 原地排序 :利用原数组进行排序,不申请新的空间。栈利用的空间应该不属于新申请的。
7. 堆排序
7.1 概括
堆排序建立在完全二叉树的基础上,分为 大根堆排序(升序)和小根堆排序(降序。每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
数组定义:
大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
7.2 思路
首先需要从一个无序数组构建一个大根堆,构造方法就是筛选(也叫下沉)。当大根堆构建完成之后,开始"输出",也就是进行堆排序。
筛选(下沉)操作:从最后一个非终端节点(n/2)开始,比较该节点的左右子树,调整为符合大根堆的顺序(只调整子树),倒序往根节点方向继续调整。
(堆排序)输出操作:筛选完成后,序列的最大值就是根节点,交换根节点与最后一个元素,然后将剩余元素重新构造筛选成一个新的堆,以此往复。
7.3 代码
public int[] heapSort(int[] a) {
int n = a.length;
// 构建初始堆
for (int i = n / 2 - 1; i >= 0; i--) {
// 从倒数第一个非终端节点开始下沉
downAdjust(a, i, n - 1);
// printArray(a);
}
for (int i = n - 1; i >= 0; i--) {
// 交换第一个和最后一个元素
int tmp = a[i];
a[i] = a[0];
a[0] = tmp;
downAdjust(a, 0, i - 1);
// 注意是 i-1,因为堆顶元素交换到末尾后不再参与排序
// printArray(a);
}
return a;
}
public void downAdjust(int[] a, int parent, int n) {
int child = parent * 2 + 1;// 左子节点
// child + 1 // 右子节点
int tmp = a[parent];//临时保存要下沉的元素
while (child <= n) {
if (child + 1 <= n && a[child] < a[child + 1]) {
//注意child + 1右子节点的下标不要越界
child++;//右子节点比左子节点大就锁定右子节点
// 目的是让下沉元素同左右子节点中最大的节点进行比较。这样才符合大根堆
}
if (a[child] <= tmp)// 不能是a[child] <= a[parent]
break;
a[parent] = a[child];//相当于和最大的子节点交换位置
parent = child;//保证子树也是大根堆
child = parent * 2 + 1;
}
a[parent] = tmp;//将下沉的元素复位
}
7.4 算法分析
堆排序也是一种选择排序。
- 时间复杂度:O(nlogn),构造初始堆为O(N),重建堆为O(logn)
- 空间复杂度:O(1)
- 非稳定排序 :为什么非稳定呢?
假设存在一个初始堆,最后一个非终端节点值为4,其右边同层处同样有一个4,那当下沉操作时,他俩的先后顺序就变了。 - 原地排序
8. 计数排序
8.1 概括
计数排序是一种适合于最大值和最小值的差值不是不是很大的排序。差距越大,需要申请的空间越大。
8.2 思路
基本思想:就是把数组元素作为数组的下标,然后用一个临时数组统计该元素出现的次数,例如 temp[i] = m, 表示元素 i 一共出现了 m 次。最后再把临时数组统计的数据从小到大汇总起来,此时汇总起来是数据是有序的。
8.3 代码
基础代码
public int[] countSort(int[] arr) {
int maxi = arr[0];
// 寻找最大值
for (int i = 1; i < arr.length; i++) {
if (arr[i] > maxi) {
maxi = arr[i];
}
}
// 统计每个元素出现的次数,元素值作为临时数组的下标
int[] tmp = new int[maxi + 1];
for (int i = 0; i < arr.length; i++) {
tmp[arr[i]]++;
}
// 将元素按照顺序和个数复原到原数组
int k = 0;// k作为一个计数器
for (int i = 0; i <= maxi; i++) {// i 就是临时数组的下标,即每个元素的值
for (int j = tmp[i]; j > 0; j--) {// j 就是每个元素的个数
arr[k++] = i;
}
}
return arr;
}
优化代码
解决最大值很大时的问题。
上面的代码中,我们是根据 max 的大小来创建对应大小的数组,假如原数组只有10个元素,并且最小值为 min = 10000,最大值为 max = 10005,那我们创建 10005 + 1 大小的数组不是很吃亏,最大值与最小值的差值为 5,所以我们创建大小为6的临时数组就可以了。
也就是说,我们创建的临时数组大小 (max - min + 1)就可以了,然后在把 min作为偏移量。优化之后的代码如下所示:
public int[] countSort2(int[] arr) {
int maxi = arr[0];
int mini = arr[0];
// 寻找最大值、最小值
for (int i = 1; i < arr.length; i++) {
if (arr[i] > maxi) {
maxi = arr[i];
}
if (arr[i] < mini) {
mini = arr[i];
}
}
// 统计每个元素出现的次数,元素值作为临时数组的下标,每个元素的下标就是arr[i]-mini
int[] tmp = new int[maxi - mini + 1];
for (int i = 0; i < arr.length; i++) {
tmp[arr[i] - mini]++;
}
// 将元素按照顺序和个数复原到原数组
int k = 0;// k作为一个计数器
for (int i = 0; i < maxi - mini + 1; i++) {// i 就是临时数组的下标,即每个元素的值(下标)
for (int j = tmp[i]; j > 0; j--) {// j 就是每个元素的个数
arr[k++] = i + mini;// 将数复原
}
}
return arr;
}
8.4 算法分析
- 时间复杂度:O(n + k), k 为临时数组下标,
- 空间复杂度:O(k)
- 稳定排序
- 非原地排序
9. 桶排序
9.0 与计数排序的区别与联系
桶排序是计数排序的升级版。
联系:
二者都是基于统计数字出现的次数。
区别:
计数排序将每个数字划分为一个桶,桶排序将一个区间内的数字划分为一个桶。
计数排序:
桶排序:
9.1 概括
桶排序就是把最大值和最小值之间的数进行瓜分,分成区间,每个区间对应一个桶,对桶内元素排序,最后合并汇总
例如分成 10 个区间,10个区间对应10个桶,我们把各元素放到对应区间的桶中去,再对每个桶中的数进行排序,可以采用归并排序,也可以采用快速排序之类的。
之后每个桶里面的数据就是有序的了,我们在进行合并汇总。
数据结构:链表里存着链表。也可以使用二维数组。
9.2 思路
那如何将元素序列划分为一个个的桶呢?即,如何划分区间呢?可以利用一个映射函数,将数字划分到对应的区间。
映射函数一般是f = array[i] / k; k^2 = n;
n是所有元素个数。
为了使桶排序更加高效,我们需要做到这两点:
1、在额外空间充足的情况下,尽量增大桶的数量;
2、使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中;
9.3 代码
public int[] bucketSort(int[] arr) {
if (arr == null || arr.length < 2) {
return arr;
}
int maxi = arr[0];
int mini = arr[0];
// 寻找最大值、最小值
for (int i = 1; i < arr.length; i++) {
if (arr[i] > maxi) {
maxi = arr[i];
}
if (arr[i] < mini) {
mini = arr[i];
}
}
// 每个桶的大小
int bucketSize = (int) Math.sqrt(arr.length);
// 桶的数量
int bucketNum = (int) Math.floor((maxi - mini) / bucketSize) + 1;
System.out.println("bucketNUm " + bucketNum);
System.out.println("size " + bucketSize);
ArrayList<LinkedList<Integer>> bucket = new ArrayList<LinkedList<Integer>>(bucketNum);
// 初始化桶
for (int i = 0; i < bucketNum; i++) {
bucket.add(new LinkedList<Integer>());
}
// 利用映射函数将元素放入桶
for (int i = 0; i < arr.length; i++) {
bucket.get((arr[i] - mini) / bucketSize).add(arr[i] - mini);
}
// 对桶内的元素进行排序
for (int i = 0; i < bucketNum; i++) {
Collections.sort(bucket.get(i));
}
// 合并汇总
int k = 0;
for (int i = 0; i < bucketNum; i++) {
for (Integer j : bucket.get(i)) {
arr[k++] = j + mini;
}
}
return arr;
}
9.4 算法分析
- 时间复杂度:O(n + k),注:k 表示桶的个数,下同
- 空间复杂度:O(k + k)
- 稳定排序
- 非原地排序
9.4.1 什么时候最快
当输入的数据可以均匀的分配到每一个桶中。
9.4.2 什么时候最慢
当输入的数据被分配到了同一个桶中。
10. 基数排序
2.1 概括
基数排序的排序思路是这样的:先以个位数的大小来对数据进行排序,接着以十位数的大小来多数进行排序,接着以百位数的大小…
排到最后,就是一组有序的元素了。不过,他在以某位数进行排序的时候,是用“桶”来排序的。
2.2 思路
由于某位数(个位/十位…,不是一整个数)的大小范围为0-9,所以我们需要10个桶,然后把具有相同数值的数放进同一个桶里,之后再把桶里的数按照0号桶到9号桶的顺序取出来,这样一趟下来,按照某位数的排序就完成了
下面是菜鸟教程上的图:
2.3 代码
public int[] radioSort(int[] arr) {
if (arr == null || arr.length < 2) {
return arr;
}
int n = arr.length;
int maxi = arr[0];
// 寻找最大值
for (int i = 1; i < n; i++) {
if (arr[i] > maxi) {
maxi = arr[i];
}
}
// 计算最大值的位数
int num = 0;
while (maxi != 0) {
maxi /= 10;
num++;
}
// 创建10个桶
ArrayList<LinkedList<Integer>> bucket = new ArrayList<>(10);
// 初始化桶
for (int i = 0; i < 10; i++) {
bucket.add(new LinkedList<Integer>());
}
// 排序:进行每一趟的排序,从个位数开始排
for (int i = 1; i <= num; i++) {
for (int j = 0; j < n; j++) {
// 获取每个元素的个位、十位、百位...
int radio = (arr[j] / (int) Math.pow(10, i - 1)) % 10;
// 放进对应桶内
bucket.get(radio).add(arr[j]);
}
// 合并放回原数组
int k = 0;
for (int j = 0; j < 10; j++) {
for (Integer t : bucket.get(j)) {
arr[k++] = t;
}
// 取出来合并了之后把桶清光数据
bucket.get(j).clear();
}
}
return arr;
}
2.4 算法分析
- 时间复杂度:O(kn)
- 空间复杂度:O(k + k)
- 稳定排序
- 非原地排序
基数排序:根据键值的每位数字来分配桶;
计数排序:每个桶只存储单一键值;
桶排序:每个桶存储一定范围的数值;
11. 总结
非稳定的排序: 选、希、快、堆