排序(7种常见排序方法)

博客介绍了常见排序算法,包括直接插入排序、希尔排序、选择排序、堆排序、冒泡排序和快速排序。阐述了各算法的排序思想、时间复杂度、空间复杂度和稳定性等特性,还对快速排序的Hoare法、挖坑法和前后指针法进行了详细分析。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

常见的排序算法:

按照插入排序分:1.直接插入排序 2.希尔排序

按照选择排序分:1.选择排序 2.堆排序

按照交换排序分:1.冒泡排序 2.快速排序

按照归并排序分:1.归并排序

常见的排序思想:是使用双指针或者三指针,在同一个数组上遍历、比较、交换。

1、直接插入排序

类比于扑克牌抽排,双指针思路。

1.时间复杂度:最坏情况下O(N^2),如54321,最好情况下O(N),如12345(只用i动,j不用走)

2.空间复杂度:O(1)

3.稳定性:稳定

4.使用场景:元素集合越接近有序,直接插入排序算法时间效率越高。

public void insertSort(int[] array){
    //[27,15,9,18,28]
    for (int i = 1; i < array.length; i++) {
        int temp = array[i];
        int j = i-1;
        for (; j >=0 ; j--) {
            if (array[j] > temp){
                array[j+1] = array[j];
            }else {
                //如果不用调整,则表示已经找到了该放的位置了
                //因为前面都有序了
                break;
            }
        }
        //当j循环走完以后,要么就是走完了所有,要么就是找到了最小位置
        //所以走完以后直接把j+1的位置赋值为temp即可
        array[j+1] = temp;
    }
}

2、希尔排序(缩小增量排序)

基本思想:先选定一个整数,把待排序文件中所有记录分成多个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。当到达 =1时,所有记录在统一组内排好序

时间复杂度:O(n^1.25)~O(1.6*n^1.25)

空间复杂度:O(1)

稳定性:不稳定

希尔排序的特性总结:

1. 希尔排序是对直接插入排序的优化。
2. gap > 1 时都是预排序,目的是让数组更接近于有序。当 gap == 1 时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
3. 希尔排序的时间复杂度不好计算,因为 gap 的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定。

跳着分的目的是让交换之后,大的数都在后面,小的数都在前面。前面的这些趟都是预排序,最后1趟才是真正的排序。每一组都是插入排序,越有序越快。

public void shellSort(int[] array){
    int gap = array.length;
    //gap表示array数组,分为多少个组
    while (gap > 1){
        //每次缩短一半的分组
        gap /= 2;
        shell(array,gap);
    }
}

/**
 * 对每组进行插入排序
 * @param array
 * @param gap
 */
public void shell(int[] array,int gap) {
    for (int i = gap; i < array.length ; i++) {
        int tmp = array[i];
        int j = i-gap;
        for (; j >=0 ; j-=gap) {
            if (array[j] > tmp){
                array[j+gap] = array[j];
            }else {
                break;
            }
        }
        array[j+gap] = tmp;
    }
}

3、选择排序

        每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。 

时间复杂度:O(n^2)

空间复杂度:O(1)

稳定性:不稳定

public void selectSort(int[] array){
    //双指针思想
    for (int i = 0; i < array.length; i++) {
        int miniIndex = i;
        for (int j = i+1; j < array.length; j++) {
            //如果j指向的数值小于最小值,那么就更新最小值索引
            if (array[j] <= array[miniIndex]){
                miniIndex = j;
            }
        }
        //找到最小值索引以后,就跟i位置交换
        swap(array,i,miniIndex);
    }
}
//3指针思想
public void doubleSelectSort(int[] array){
    int left = 0;
    int right = array.length-1;

    while (left < right){
        int minIndex = left;
        int maxIndex = left;
        //走一趟left和right之间的值
        for (int i = left + 1;i<= right;i++){
            if (array[i] < array[minIndex]){
                minIndex = i;
            }
            if (array[i] > array[maxIndex]){
                maxIndex = i;
            }
        }
        //注意,如果最大值在第一个位置,会出现bug
        //如果最大值在下标为0的位置,此时第一个swap会把最大值换到minIndex位置上
        //而第二个swap会把0位置的最小值换到数组最后一个位置,就乱了
        swap(array,minIndex,left);
        //如果最大值的索引,正好等于left
        //交换完成以后,left存储的是最小值,minIndex存储的是最大值
        //所以需要多加一步,将minIndex赋给maxIndex
        if (maxIndex == left){
            maxIndex = minIndex;
        }
        swap(array,maxIndex,right);
        left++;
        right--;
    }
}

4、堆排序

利用堆的特性来排序。排升序是用大根堆,排降序是用小根堆。其实就是不断地将堆中的树进行向上升序或者向下排序,由于堆中的元素是大根堆或者小根堆排列的,所以最终的树就是有序的了。

时间复杂度:O(N*logN)

空间复杂度:O(1)

稳定性:不稳定

public void heapSort(int[] array){
    //先把数组创建成堆
    createHeap(array);

    //从最后一棵树开始排序
    int end = array.length-1;
    while (end > 0){
        swap(array,0,end);
        siftDown(array,0,end);
        end--;
    }

}

private void siftDown(int[] array,int parent, int lenght) {
    int child = 2 * parent + 1;
    while (child < lenght){
        if (child + 1<lenght && array[child] < array[child+1]){
            child++;
        }
        if (array[child] > array[parent]){
            swap(array,child,parent);
            parent = child;
            child = 2*parent+1;
        }else {
            break;
        }

    }
}

private void createHeap(int[] array) {
    //从最后一个父亲结点开始创建
    for (int parent = (array.length-1-1)/2; parent >=0 ; parent--) {
        siftDown(array,parent,array.length);
    }
}

 5、冒泡排序

双指针

时间复杂度:O(n^2),优化以后最好情况下可以达到O(n)

空间复杂度:O(1)

稳定性:稳定

public void bubbleSort(int[] array){
        for (int i = 0; i < array.length-1; i++) {
            boolean flg = false;
            for (int j = 0; j < array.length-1-i; j++) {
                if (array[i] > array[j]){
                    swap(array,i,j);
                    flg = true;
                }
            }
            if (!flg){
                break;
            }
        }
    }

6、快速排序

        任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有 元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止

时间复杂度:最好的情况下,O(N*logN),当最坏的情况下,O(N^2),逆序或者有序

空间复杂度:O(logN)

稳定性:不稳定    

上述为快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像,同学们在写递归框架时可想想二叉树前序遍历规则即可快速写出来,后序只需分析如何按照基准值来对区间中数据进行划分的方式即可。

其实最核心的任务就是找到相交点,交换。

6.1 Hoare法

递归以后层数多了可能会栈溢出。因为怕就怕他切分的这棵树,左边长右边短,或者左边短右边长。如果是均匀切分的话,其实是效果比较好的。

private void quick(int[] array,int start,int end){
    if (start >= end){
        return;
    }

    //每次相遇的位置
    int pivot = partitionHoare(array,start,end);

    //以相遇的位置作为分割点,切分以后递归继续找
    quick(array,start,pivot-1);
    quick(array,pivot+1,end);
}

private int partitionHoare(int[] array, int left, int right) {
    //待比较元素
    int tmp = array[left];
    int i =left;

    while (left < right){
        //单独的循环,不能一直减到超过左边的边界
        while (left < right && array[right] >= tmp){
            right--;
        }
        while (left < right && array[left] <= tmp){
            left ++;
        }
        //交换left和right找到的值
        swap(array,left,right);
    }
    //此时left和right都在相遇点了,将相遇点换到最前面
    swap(array,i,left);
    return left;
}

6.2 挖坑法(Hoare衍生出来的)

        挖坑法就是把基准从数组中拿出来,数组中就留下了一个坑,然后同样从右边和左边遍历,遇到大于或者小于基准的就把那个元素,拿到坑这个位置放下,反复循环,知道左右索引相遇,就把基准填入最后一个坑里。

        

    private int partitionHole(int[] array,int left,int right){
        //待比较元素
        int tmp = array[left];

        while (left < right){
            //单独的循环,不能一直减到超过左边的边界
            while (left < right && array[right] >= tmp){
                right--;
            }
            //right找到比基准小的值,就把right所指向的元素填入left位置
            //因为此时left位置就是坑
            array[left] = array[right];

            while (left < right && array[left] <= tmp){
                left ++;
            }
            array[right] = array[left];
        }
        array[left] = tmp;
        return left;
    }

值得注意的2个细节:

1,array[right] >= tmp 为什么要加等于?

如果不加等于,会死循环。因为left=right,left无法++right无法--,会不停地交换这两个值。

2,为什么从右边开始而不是先从左边开始?

如果先让left先走,那么走到相遇地方的时候,left指向的是一个比较大的数,此时将left和最左边交换,会让这个大的数换到最左边去。而如果是从右边先开始的话,right不断--,当遇到left的时候,right走到的是一个较小的数,此时再与最左边交换,就没问题。

6.3 前后指针法

cur用来指向比基准小的数,prev用来推着比基准大的数,cur不停地找比基准小的数,去和prev指向的比基准大的数交换。给人的感觉就是prev在不停地推着这些比基准大的数往前滚动,cur作为开路先锋去找小的数来交换。 

//前后指针法1
    private  int partitionBFPoint1(int[] array, int left, int right) {
        int prev = left ;
        int cur = left+1;
        while (cur <= right) {
            if(array[cur] < array[left] && array[++prev] != array[cur]) {
                swap(array,cur,prev);
            }
            cur++;
        }
        swap(array,prev,left);
        return prev;
    }
//前后指针法2
    private  int partitionBFPoint2(int[] array, int left, int right) {
        int d = left + 1;
        int pivot = array[left];
        for (int i = left + 1; i <= right; i++) {
            if (array[i] < pivot) {
                swap(array, i, d);
                d++;
            }
        }
        swap(array, d - 1, left);
        return d - 1;
    }

6.4 快速排序小结

三种快速排序的方法:

1.Hoare法

2.挖坑法

3.前后指针法

这三种方法每次划分以后的序列顺序,有可能是不一样的。

当面临一个序列,要你判断他用的哪一种快速排序的方法的时候。根据以往的经验,先后检验的顺序是挖坑法、Hoare法、前后指针法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值