开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第16天,点击查看活动详情
本次涉及排序算法有冒泡排序,选择排序,插入排序,希尔排序,归并排序,快排,堆排序,计数排序,桶排序,基数排序。
关于冒泡排序,桶排序,插入排序,快排参考文章: 关于排序算法的一些总结 - 掘金 (juejin.cn)
关于时间复杂度,空间复杂度
| 算法 | 平均时间复杂 | 最少时间 | 最多时间 | 空间 | 稳定 |
|---|---|---|---|---|---|
| 冒泡 | O(n^2) | O(n) | O(n^2) | O(1) | |
| 选择 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
| 插入 | O(n^2) | O(n^2) | O(n^2) | O(1) | |
| 希尔 | O(n log n) | O(n log^2 n) | O(n log^2 n) | O(1) | 不稳定 |
| 归并 | O(n log n) | O(n log n) | O(n log^2 n) | O(n) | |
| 快排 | O(n log n) | O(n log n) | O(n^2) | O(log n) | 不稳定 |
| 堆 | O(n log n) | O(n log n) | O(n log n) | O(1) | 不稳定 |
| 计数 | O(n+k) | O(n+k) | O(n+k) | O(k) | |
| 桶 | O(n+k) | O(n+k) | O(n^2) | O(n+k) | |
| 基数 | O(nXk) | O(nXk) | O(nXk) | O(n+k) |
1.基数排序
基数排序基本思想是依次选择位数进行排序。 分为
- MSD:先从高位开始进行排序,在每个关键字上,可采用计数排序
- LSD:先从低位开始进行排序,在每个关键字上,可采用桶排序
以LSD为例,假设原来有一串数值如下所示: 73, 22, 93, 43, 55, 14, 28, 65, 39, 81
首先按照个位数值,在走访数值时将它们分配至编号0到9的桶子中
接下来将这些桶子中的数值重新串接起来,成为以下的数列: 81, 22, 73, 93, 43, 14, 55, 65, 28, 39
接着再进行一次分配,这次是根据十位数来分配:
接下来将这些桶子中的数值重新串接起来,成为以下的数列: 14, 22, 28, 39, 43, 55, 65, 73, 81, 93
这时候整个数列已经排序完毕;如果排序的对象有三位数以上,则持续进行以上的动作直至最高位数为止。
LSD的基数排序适用于位数小的数列,如果位数多的话,使用MSD的效率会比较好。MSD的方式与LSD相反,是由高位数为基底开始进行分配,但在分配之后并不马上合并回一个数组中,而是在每个“桶子”中建立“子桶”,将每个桶子中的数值按照下一数位的值分配到“子桶”中。在进行完最低位数的分配后再合并回单一的数组中。
我们可以看出来:有几个位,就会进行几次排序,缺排序结果和排序之前的顺序没有任何关系。
其代码步骤如下:
得到数组中最大数的位数,确定需要排几轮;
定义一个长度为10的二维数组模拟10个桶,其中每个桶的长度为原数组的长度;
定义一个一维数组用来记录每一轮的每个桶中放了多少个数据;
每一轮先按每个数个位、十位、百位.的顺序放入桶中;放完之后再按桶的顺序将每个数放回到原数组。直至排完
/*
* 基数排序
*/
int[] array = { 73, 22, 93, 43, 55, 14, 28, 65, 39, 81 };
public static void radixSort(int[] array) {
//拿到最大数
int max=array[0];
for(int i=0;i<array.length;i++){
if(array[i]>max)
{
max=array[i];
}
}
//得到最大数的位数,确定循环几轮
int maxLength = (max + "").length();
//定义一个二维数组用来模拟10个
int[][] bucket=new int[10][array.length];
//定义一个一维数组用来记录每一轮的每个桶中放了多少个数据
int[] bucketEleCounts=new int[10];
//开始排序 n专门用来辅助得到每轮的位数
for(int i=0,n=1;i<maxLength;i++,n*=10){
for(int j=0;j<array.length;j++){
//得到需要的位数
int ele=array[j]/n%10;
//放进桶
bucket[ele][bucketEleCounts[ele]] = array[j];
//第ele个桶的数量+1
bucketEleCounts[ele]++;
}
//按桶的顺序依次把数据取出来
int index=0;
for(int k=0;k<bucket.length;k++){
if (bucketEleCounts[k] != 0) {
// 第k个桶里面的数据循环取出来
for (int l = 0; l < bucketEleCounts[k]; l++) {
array[index] = bucket[k][l];
index++;
}
//桶的数据个数清空
bucketEleCounts[k] = 0;
}
}
}
选择排序
每次从一端开始,选择最大或者最小的,存放到排序序列的起始位置。 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。看上去很像冒泡,是向前冒泡
代码实现:
public int[] sort(int[] arr){
for(int i=0;i<arr.length;i++){
int min=i;
for(int j=i+1;j<array.length;j++){
if(arr[j]<arr[min]){
min=j;
}
}
if (i != min) {
int tmp = arr[i];
arr[i] = arr[min];
arr[min] = tmp;
}
}
return arr;
}
希尔排序:
希尔排序(Shell Sort)是插入排序的一种,它是针对直接插入排序算法的改进。 希尔排序又称缩小增量排序。 它通过比较相距一定间隔的元素来进行,各趟比较所用的距离随着算法的进行而减小,直到只比较相邻元素的最后一趟排序为止。
即:若增量为3,第i、i+3、i+6个元素都为有序序列(i=1,2,3)
代码实现: 希尔排序目的为了加快速度改进了插入排序,交换不相邻的元素对数组的局部进行排序,并最终用插入排序将局部有序的数组排序。
在此我们选择增量 gap=length/2,缩小增量以 gap = gap/2 的方式,用序列 {n/2,(n/2)/2...1} 来表示。
初始增量第一趟 gap = length/2 = 4。 第二趟,增量缩小为 2。 第三趟,增量缩小为 1,得到最终排序结果。
public void sort(Comparable[] arr){
int j;
for(int gap=arr.length/2;gap>0;gap/=2){
for (int i = gap; i < arr.length; i++) {
Comparable tmp = arr[i];
for (j = i; j >= gap && tmp.compareTo(arr[j - gap]) < 0; j -= gap) {
arr[j] = arr[j - gap];
}
arr[j] = tmp;
}
}
}
归并排序:
采用分治法(Divide and Conquer)的一个非常典型的应用。
作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:
- 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
- 自下而上的迭代;
看上去有点模糊,图示如下
代码:
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
- 重复步骤 3 直到某一指针达到序列尾;
- 将另一序列剩下的所有元素直接复制到合并序列尾。
public int[] sort(int[] arr){
if(arr.length<2){
return arr;
}
int moddle=(int) Math.floor(arr.length / 2);
//分成两部分
int[] left = Arrays.copyOfRange(arr, 0, middle);
int[] right = Arrays.copyOfRange(arr, middle, arr.length);
return merge(sort(left), sort(right));
}
int[] merge(int[] left,int[] right){
int[] res=new int[left.length+right.length];
while(left.length > 0 && right.length > 0) {
if(left[0]<=right[0]){
res[i++]=left[0];
left=Arrays.copyOfRange(left, 1, left.length);
}
else{
res[i++]=right[0];
right = Arrays.copyOfRange(right, 1, right.length);
}
}
while(left.length>0){
res[i++]=left[0];
left=Arrays.copyOfRange(left, 1, left.length);
}
while(right.length>0){
res[i++]=right[0];
right=Arrays.copyOfRange(right, 1, left.length);
}
return res;
}
堆排序
堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:
- 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
- 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列
下面以大顶堆为例
第一步:把最后一个数和根交换,然后进行排序
代码:
//构建大根堆:将array看成完全二叉树的顺序存储结构
int[] buildMaxHeap(int[] array){
//从最后一个节点array.length-1的父节点(array.length-1-1)/2开始,直到根节点0,反复调整堆
for(int i=(array.length-2)/2;i>=0;i--){
adjustDownToUp(array, i,array.length);
}
return array;
}
//调整数:
void heapify(int[] arr, int i, int len) {
int left = 2 * i + 1;
int right = 2 * i + 2;
int largest = i;
if (left < len && arr[left] > arr[largest]) {
largest = left;
}
if (right < len && arr[right] > arr[largest]) {
largest = right;
}
if (largest != i) {
swap(arr, i, largest);
heapify(arr, largest, len);
}
}
void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
//sort代码
public int[] sort(int[] arr) {
int len = arr.length;
buildMaxHeap(arr, len);
for (int i = len - 1; i > 0; i--) {
swap(arr, 0, i);
len--;
heapify(arr, 0, len);
}
return arr;
}
不稳定算法:即当有两个相同数时候,相对位置就会变化
希尔,快排,堆,选择