十大排序算法(带注释)
术语
- 时间复杂度:算法执行所消耗的时间。
- 空间复杂度:算法执行所消耗的存储空间。
- 稳定排序:相同的两个元素,排序前后顺序不变,用于两个排序关键字的情况,如对价格升序的同时销量也升序。
- 不稳定排序:相同的两个元素,排序前后顺序可能发生改变,如快速排序、希尔排序、选择排序、堆排序(快些选堆)。
- 原地排序:无需额外存储空间,直接在原数组上进行排序,所以会修改原数组。
- 非原地排序:需要利用额外的存储空间来辅助排序。
一、选择排序:选择最小元素与首元素交换
1、找到数组中最小的那个元素,将它和数组的第一个元素交换位置;
2、这样一趟比较下来,排在第一的元素就会是最小的数;
2、在剩下的元素中找到最小的元素,将它和数组的第二个元素交换位置;
3、如此往复,直到将整个数组排序。
/**
* <dl>
* <dt><b>1.选择排序</b></dt>
* <dd>每一轮选出最小值,交换到左侧</dd>
* </dl>
* @param a 待排序数组
*/
public static void selectSort(int[] a) {
int n = a.length;
int minIndex = 0;
// 只需比较n-1轮
for (int i = 0; i < n - 1; i++) {
minIndex = i;
// 每轮比较n-1-i次
for (int j = i + 1; j < n; j++) {
if (a[j] < a[i]) {
// 最小值下标
minIndex = j;
}
}
// 最小值与a[i]交换
int temp = a[i];
a[i] = a[minIndex];
a[minIndex] = temp;
}
}
性质:
- 时间复杂度:O(n2)
- 空间复杂度:O(1)
- 不稳定排序
- 原地排序
二、插入排序:将元素插入到不比它大的位置(插扑克牌)
1、从数组第2个元素开始抽取元素;
2、把它与左边第一个元素比较,如果左边第一个元素比它大,则继续与左边第二个元素比较下去,直到遇到不比它大的元素,然后插到这个元素的右边;
3、这样一趟比较下来,该元素已经被插入到适当的位置,其左侧都不比它大;
4、继续选取第3,4,….n个元素,重复步骤 2 ,选择适当的位置插入。
/**
* <dl>
* <dt><b>2.插入排序</b></dt>
* <dd>维护一个有序区,把元素一个一个插入到有序区的适当位置,直到所有元素有序为止</dd>
* </dl>
* @param a 待排序数组
*/
public static void insertSort(int[] a) {
int n = a.length;
int j = 0;
int insValue = 0;
// 只需比较n-1轮
for (int i = 0; i < n - 1; i++) {
// 假定a[0]有序,从a[1]开始
j = i + 1;
// 待插入值
insValue = a[j];
// 比待插入值大的元素(待插入值前面的)后移,给待插入值腾出位置
while (j > 0 && insValue < a[j - 1]) {
a[j] = a[j - 1];
j -= 1;
}
// 把待插入值放在适当位置上
a[j] = insValue;
}
}
性质:
- 时间复杂度:O(n2)
- 空间复杂度:O(1)
- 稳定排序
- 原地排序
三、冒泡排序:元素两两比较,最大元素交换到末尾
1、把第一个元素与第二个元素比较,如果第一个比第二个大,则交换他们的位置;
2、接着继续比较第二个与第三个元素,如果第二个比第三个大,则交换他们的位置;
3、这样一趟比较下来,排在最后的元素就会是最大的数;
4、如此往复,直到将整个数组排序。
/**
* <dl>
* <dt><b>3.冒泡排序(标准版)</b></dt>
* <dd>两两比较,左比右大就交换,最大的数冒泡到右侧</dd>
* </dl>
* @param a 待排序数组
*/
public static void bubbleSort(int[] a) {
int n = a.length;
// 交换用临时变量
int temp = 0;
// 只需比较n-1轮
for (int i = 0; i < n - 1; i++) {
// 每轮比较n-1-i次
for (int j = 1; j < n; j++) {
if (a[j - 1] > a[j]) {
temp = a[j - 1];
a[j - 1] = a[j];
a[j] = temp;
}
}
}
}
性质:
- 时间复杂度:O(n2)
- 空间复杂度:O(1)
- 不稳定排序
- 原地排序
假如从开始的第一对到结尾的最后一对,相邻的元素之间都没有发生交换的操作,这意味着右边的元素总是大于等于左边的元素,此时的数组已经是有序的了,我们无需再对剩余的元素重复比较下去了。
优化之后的代码如下所示:
/**
* <dl>
* <dt><b>3.冒泡排序(优化版)</b></dt>
* <dd>两两比较,左比右大就交换,最大的数冒泡到右侧</dd>
* <dd>1.如果一轮比较后没有交换,那么数组已经有序</dd>
* <dd>2.每排序一轮,内层循环的次数可以减1</dd>
* <dd>3.用异或交换值</dd>
* </dl>
* @param a 待排序数组
*/
public static void bubbleSortOptimization(int[] a) {
// 优化2:内层循环次数
int loops = a.length;
// 优化1:交换标志
boolean swapFlag = false;
// 只需比较n-1轮
for (int i = 0; i < a.length - 1; i++) {
// 每轮循环都要初始化交换标志
swapFlag = false;
// 每轮比较loops - 1次
for (int j = 1; j < loops; j++) {
if (a[j - 1] > a[j]) {
// 优化3:用异或交换值
a[j - 1] = a[j - 1] ^ a[j];
a[j] = a[j] ^ a[j - 1];
a[j - 1] = a[j - 1] ^ a[j];
// 已交换
swapFlag = true;
}
}
// 缩减内层循环次数
loops--;
// 本轮没有交换,结束
if (!swapFlag) {
return;
}
}
}
四、希尔排序:将插入排序的1替换成逐步折半的step
希尔排序就是为了加快速度简单地改进了插入排序,交换不相邻的元素以对数组的局部进行排序。
1、让数组中任意间隔为 step = n / 2 的元素有序;
2、让数组中任意间隔为 step = n / 4 的元素有序;
3、直到数组中任意间隔为 step = 1 的元素有序,此时整个数组就是有序的。
/**
* <dl>
* <dt><b>4.希尔排序</b></dt>
* <dd>将插入排序的1替换成逐步折半的step</dd>
* </dl>
* @param a 待排序数组
*/
public static void shellSort(int[] a) {
int n = a.length;
int j = 0;
int insValue = 0;
// 采用逐步折半的增量方法
for (int step = n / 2; step > 0; step /= 2) {
// 只需比较n-1轮
for (int i = 0; i < n - step; i++) {
// 假定a[0]有序,从a[step]开始
j = i + step;
// 待插入值
insValue = a[j];
// 比待插入值小的元素(待插入值前面的)后移,给待插入值腾出位置
while (j > 0 && insValue < a[j - step]) {
a[j] = a[j - step];
j -= step;
}
// 把待插入值放在适当位置上
a[j] = insValue;
}
}
}
性质:
- 时间复杂度:O(nlogn)
- 空间复杂度:O(1)
- 不稳定排序
- 原地排序
五、归并排序:将左右数组排序,然后归并
1、通过递归的方式将大的数组一直分割,直到数组的大小为 1,此时只有一个元素,那么该数组就是有序的了;
2、把两个数组大小为1的合并成一个大小为2的数组;
3、把两个数组大小为2的合并成一个大小为4的数组;
4、直到全部小的数组合并起来,此时整个数组就是有序的。
/** 辅助数组 */
private static int[] aux = null;
/**
* <dl>
* <dt><b>5.归并排序</b></dt>
* <dd>将左右数组排序,然后归并</dd>
* </dl>
* @param a 待排序数组
*/
public static void mergeSort(int[] a) {
// 辅助数组分配内存空间
aux = new int[a.length];
// 自顶向下
// upToDown(a, 0, a.length - 1);
// 自底向上
downToUp(a);
}
/**
* <dl>
* <dt><b>5.归并排序(自顶向下)</b></dt>
* <dd>递归调用</dd>
* </dl>
* @param a 待排序数组
* @param left 要排序的最小下标
* @param right 要排序的最大下标
*/
public static void upToDown(int[] a, int left, int right) {
// 左右指针相遇时返回
if (left >= right) {
return;
}
int mid = left + (right - left) / 2;
// 将左边排序
upToDown(a, left, mid);
// 将右边排序
upToDown(a, mid + 1, right);
// 归并
merge(a, left, mid, right);
}
/**
* <dl>
* <dt><b>5.归并排序(自底向上)</b></dt>
* <dd>先两两归并,再四四归并,以此类推</dd>
* </dl>
* @param a 待排序数组
*/
public static void downToUp(int[] a) {
int n = a.length;
// a[0...sz...sz+sz-1]排序,a[sz+sz...sz+sz+sz-1...sz+sz+sz+sz-1]排序
// 每次都是sz+sz个元素排序
for (int sz = 1; sz < n; sz = sz + sz) {
for (int left = 0; left < n - sz; left += sz + sz) {
merge(a, left, left + sz - 1, Math.min(left + sz + sz - 1, n - 1));
}
}
}
/**
* <dl>
* <dt><b>5.归并排序(归并)</b></dt>
* <dd>先将数组复制到辅助数组,然后左右数组分别归并到原数组</dd>
* </dl>
* @param a 待排序数组
* @param left 要排序的最小下标
* @param mid 中间下标
* @param right 要排序的最大下标
*/
public static void merge(int[] a, int left, int mid, int right) {
for (int i = left; i <= right; i++) {
// 辅助数组初始化
aux[i] = a[i];
}
int i = left;
int j = mid + 1;
// 归并回原数组
for (int k = left; k <= right; k++) {
if (i > mid) {
// 左边用尽,取右边元素
a[k] = aux[j++];
} else if (j > right) {
// 右边用尽,取左边元素
a[k] = aux[i++];
} else if (aux[i] > aux[j]) {
// 取最小的,即右边元素
a[k] = aux[j++];
} else {
// 取最小的,即左边元素
a[k] = aux[i++];
}
}
}
性质:
- 时间复杂度:O(nlogn)
- 空间复杂度:O(n)
- 稳定排序
- 非原地排序
六、快速排序:选择一个切分元素使得左边的元素都小于它,右边的元素都不小于它
1、选择一个切分元素,暂存到tmp变量;
2、交换左边大于tmp和右边小于tmp的元素;
3、将切分元素与左右相遇位置元素交换;
4、如此往复,直到将整个数组排序。
/**
* <dl>
* <dt><b>6.快速排序</b></dt>
* <dd>选择一个切分元素,暂存到tmp变量中</dd>
* <dd>使得左边的元素都比他小,右边的都比它大</dd>
* <dd>如此循环,直到左右指针相遇</dd>
* </dl>
* @param a 待排序数组
* @param left 待排序的最小下标
* @param right 待排序的最大下标
*/
public static void quickSort(int[] a, int left, int right) {
// 左右指针相遇时返回
if (left >= right) {
return;
}
// 切分元素的下标
int mid = partition(a, left, right);
// 继续对左侧切分
quickSort(a, left, mid - 1);
// 继续对右侧切分
quickSort(a, mid + 1, right);
}
/**
* @param a 待排序数组
* @param left 待排序的最小下标
* @param right 待排序的最大下标
* @return 切分元素下标
*/
public static int partition(int[] a, int left, int right) {
// 暂存切分元素
int pivot = a[left];
int i = left;
int j = right + 1;
int tmp = 0;
while (true) {
// 找到左侧大于切分元素的下标
while (a[++i] <= pivot) {
// 直到最大下标也没找到
if (i == right) {
break;
}
}
// 找到右侧小于切分元素的下标
while (a[--j] >= pivot) {
// 直到最小下标也没找到
if (j == left) {
break;
}
}
// 左右指针相遇,结束
if (i >= j) {
break;
}
// 交换a[i]和a[j]
tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}
// 交换a[left]和a[j]
a[left] = a[j];
// 使中轴元素处于有序的位置
a[j] = pivot;
return j;
}
性质:
- 时间复杂度:O(nlogn)
- 空间复杂度:O(logn)
- 不稳定排序
- 原地排序
七、堆排序:将无序数组构建成排序二叉堆
1、把无序数组构建成二叉堆;
2、堆顶堆底元素互换;
3、调节打乱后的堆,产生新的堆顶。
/**
* <dl>
* <dt><b>7.堆排序</b></dt>
* <dd>把无序数组构建成二叉堆</dd>
* <dd>循环删除堆顶元素,移到集合尾部,调节打乱后的堆,产生新的堆顶</dd>
* </dl>
* @param a 待排序数组
*/
public static void heapSort(int[] a) {
int n = a.length;
// 把无序数组构建成二叉堆
for (int i = (n - 2) / 2; i >= 0; i--) {
downAdjust(a, i, n);
}
int tmp = 0;
// 排序二叉堆
for (int i = n - 1; i > 0; i--) {
// 堆顶堆底元素互换
tmp = a[0];
a[0] = a[i];
a[i] = tmp;
// 调节打乱后的堆,产生新的堆顶
downAdjust(a, 0, i);
}
}
/**
* <dl>
* <dt><b>下沉调整</b></dt>
* </dl>
* @param a 待调整的堆
* @param parent 待下沉的父节点
* @param length 堆的有效大小
*/
public static void downAdjust(int[] a, int parent, int length) {
// 保存父节点的值
int tmp = a[parent];
// 左子节点
int child = parent * 2 + 1;
while (child < length) {
if (child + 1 < length && a[child + 1] > a[child]) {
// 指向最大的那个子节点
child++;
}
// 如果父节点大于等于最大子节点的值,无需下沉,跳出
if (tmp >= a[child]) {
break;
}
// 子节点上浮
a[parent] = a[child];
// 当前子节点变为父节点
parent = child;
// 当前子节点的子节点
child = child * 2 + 1;
}
// 父节点移动到合适的位置
a[parent] = tmp;
}
性质:
- 时间复杂度:O(nlogn)
- 空间复杂度:O(1)
- 不稳定排序
- 原地排序
八、计数排序:统计字母个数,创建长度27的数组,每个元素表示字母出现的次数
计数排序是一种适合于最大值和最小值的差值不是不是很大的排序。
1、假设数组取值范围[0, max],那么创建一个长度为max+1的计数数组;
2、数值出现一次,计数数组对应元素值+1,然后将计数数组写回原数组。
/**
* <dl>
* <dt><b>8.计数排序(标准版)</b></dt>
* <dd>假设数组取值范围[0, max],那么创建一个长度为max+1的计数数组</dd>
* <dd>数值出现一次,计数数组对应元素值+1,然后将计数数组写回原数组</dd>
* @param a 待排序数组
* </dl>
*/
public static void countSort(int[] a) {
int max = a[0];
for (int i = 1; i < a.length; i++) {
// 找出最大值
if (a[i] > max) {
max = a[i];
}
}
// 计数数组
int[] counts = new int[max + 1];
for (int i : a) {
// 计数
counts[i]++;
}
int index = 0;
for (int i = 0; i < counts.length; i++) {
while (counts[i] > 0) {
// 回写
a[index++] = i;
// 计数减1
counts[i]--;
}
}
}
性质:
- 时间复杂度:O(n + k)
- 空间复杂度:O(k)
- 稳定排序
- 非原地排序
注:k 表示临时数组的大小
上面的代码中,我们是根据 max 的大小来创建对应大小的数组,假如原数组只有10个元素,并且最小值为 min = 10000,最大值为 max = 10005,那我们创建 10005 + 1 大小的数组不是很吃亏,最大值与最小值的差值为 5,所以我们创建大小为6的临时数组就可以了。
也就是说,我们创建的临时数组大小 (max - min + 1)就可以了,然后在把 min作为偏移量。优化之后的代码如下所示:
/**
* <dl>
* <dt><b>8.计数排序(优化版)</b></dt>
* <dd>计数数组的大小设置为max-min+1</dd>
* @param a 待排序数组
* </dl>
*/
public static void countSortOptimization(int[] a) {
int max = a[0];
int min = a[0];
for (int i = 1; i < a.length; i++) {
// 找出最值
max = Math.max(a[i], max);
min = Math.min(a[i], min);
}
// 计数数组
int[] counts = new int[max - min + 1];
for (int i : a) {
// 计数
counts[i - min]++;
}
int index = 0;
for (int i = 0; i < counts.length; i++) {
while (counts[i] > 0) {
// 回写
a[index++] = i + min;
// 计数减1
counts[i]--;
}
}
}
九、桶排序:以十位上的数字划分0~9共10个桶,对应数字分别放入桶中进行排序,然后再进行归并
1、创建(max-min)/length+1个桶,相应地将i元素放入(a[i]-min)/length桶中;
2、再对每个桶进行排序,可以使用快速排序、归并排序等;
3、将排序后的桶依次写回数组,此时整个数组就是有序的。
/**
* <dl>
* <dt><b>9.桶排序</b></dt>
* <dd>创建(max-min)/length+1个桶,相应地将i元素放入(a[i]-min)/length桶中</dd>
* <dd>再对每个桶进行排序,最后将排序后的桶依次写回数组</dd>
* </dl>
* @param a 待排序数组
*/
public static void bucketSort(int[] a) {
int min = Integer.MAX_VALUE;
int max = Integer.MIN_VALUE;
for (int i : a) {
min = Math.min(min, i);
max = Math.max(max, i);
}
int n = a.length;
int bucketLength = (max - min) / n + 1;
// 用ArrayList作为桶,每个桶里面还是一个ArrayList
ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketLength);
for (int i = 0; i < bucketLength; i++) {
bucketArr.add(new ArrayList<Integer>());
}
int bucketIndex = 0;
for (int i = 0; i < n; i++) {
// 对应桶下标
bucketIndex = (a[i] - min) / n;
// 放入对应桶
bucketArr.get(bucketIndex).add(a[i]);
}
int index = 0;
// 可以使用各种方法对每个桶排序,并放入数组
for (ArrayList<Integer> arrayList : bucketArr) {
Collections.sort(arrayList);
for (int i : arrayList) {
a[index] = i;
index++;
}
}
}
性质:
- 时间复杂度:O(n + k)
- 空间复杂度:O(n + k)
- 稳定排序
- 非原地排序
注:k 表示桶的个数
十、基数排序:从个位开始分别放入桶中,然后写回数组,个位、十位依次有序
1、个位放入对应的桶,再将桶中数据写回数组,此时个位有序;
2、十位放入对应的桶,再将桶中数据写回数组,此时十位有序;
3、如此往复,直到将整个数组排序。
/**
* <dl>
* <dt><b>10.基数排序</b></dt>
* <dd>个位放入对应的桶,清空桶,十位放入对应的桶,清空桶</dd>
* </dl>
* @param a 待排序数组
*/
public static void radixSort(int[] a) {
// 10进制
int radix = 10;
int n = a.length;
int max = Integer.MIN_VALUE;
for (int i : a) {
// 最大值
max = Math.max(max, i);
}
for (int i = 1; max / i > 0; i *= radix) {
int[][] buckets = new int[radix][n];
int remainder = 0;
for (int j = 0; j < n; j++) {
// 求当前位
remainder = (a[j] / i) % radix;
// 放入对应的桶中
buckets[remainder][j] = a[j];
}
// 桶中数据放入数组,第一轮个位有序,第二轮十位有序
int k = 0;
for (int[] bucket : buckets) {
for (int buc : bucket) {
if (buc != 0) {
a[k] = buc;
k++;
}
}
}
}
}
性质:
- 时间复杂度:O(n * k)
- 空间复杂度:O(n + k)
- 稳定排序
- 非原地排序
注:k 表示桶的个数
算法总结
排序算法 | 平均时间复杂度 | 最好 | 最坏 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
插入排序 | O(n^2) | O(n) | O(n^2) | O(1) | 稳定 |
冒泡排序 | O(n^2) | O(n) | O(n^2) | O(1) | 稳定 |
希尔排序 | O(n^1.3) | O(n) | O(nlog2n) | O(1) | 不稳定 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 |
快速排序 | O(nlogn) | O(nlogn) | O(n^2) | O(logn) | 不稳定 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
计数排序 | O(n+k) | O(n+k) | O(n+k) | O(k) | 稳定 |
桶排序 | O(n+k) | O(n+k) | O(n+k) | O(n+k) | 稳定 |
基数排序 | O(n*k) | O(n*k) | O(n*k) | O(n+k) | 稳定 |