数据结构:排序干货!(7大排序汇总+快速排序的优化+计数排序+基数排序+桶排序)

目录

概念

插入排序

直接插入排序

希尔排序

选择排序

直接选择排序

双向选择排序

堆排序

交换排序

冒泡排序

快速排序

Hoare法

挖坑法

前后指针法

快排的优化

三数取中法

非递归快排

归并排序

分治算法+二路归并

非递归归并

应用

排序总结

其他排序

计数排序

简单版本

复杂版本(稳定版本)

基数排序 

桶排序 


概念

排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。

稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。  

内部排序:数据元素全部放在内存中的排序

外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。 (比如要放在磁盘,硬盘等来进行排序)

分类:


插入排序

直接插入排序

动图演示如下:在这里插入图片描述

我们可以设置两个指针i和j,i放在第二个元素的位置,j放在第一个元素的位置

每次把i位置的元素提取出来放到tmp中,和j位置的元素进行比较,如果tmp的元素较小,就与j位置元素进行交换

交换完之后j--,看看前面还有没有元素比交换后靠前的元素大,如果有就重复上述步骤,没有就把j和i挪到下一个元素

 

    public static void insertSort(int[] array) {
        for (int i = 1; i < array.length; i++) {
            int tmp = array[i];
            int j = i - 1;
            for (; j >= 0; j--) {
                if (array[j] > tmp) {
                    array[j + 1] = array[j];
                } else {
                    break;
                }
            }
            array[j + 1] = tmp;
        }
    }

时间复杂度?

最坏情况:

假设有n个数据,遍历第2个元素并进行交换,j要回退2-1 = 1次

遍历第3个元素并进行交换,j要回退3-1 = 2次

遍历第n个元素并进行交换,j还需要进行回退n-1次,那么需要n-1次操作,

总共就需要

1+2+3+...+n-1 ≈ n^2 -->O(n^2)

最好情况:

交换之后j不需要进行回退,那么直直遍历下去 --> O(n)

所以直插适用于:待排序序列基本趋于有序了

稳定性?

直接插入排序是稳定的,数据交换判断需要array[i] > tmp才有进行交换,如果原本序列有两个相同的数字,那直插是不会改变这两个数字的顺序的,所以是稳定的

⚠一个稳定的排序可以通过修改条件变成不稳定的排序,但是不稳定的排序一定不能变成稳定的排序


希尔排序

缩小增量排序,比如下面的排序

初始数据我们可以分成5组,此时的增量就是5,接着第1个元素与第6个元素,第2个元素与第7个元素等两两交换

接着降低增量(gap / 2),增加每组的数据,继续进行排序

其实前面的增量分组相当于一个预排序,真正的排序是最后一组

    //希尔排序
    public static void shellSort(int[] array){
        int gap = array.length;
        while(gap>1){
            gap /= 2;
            shell(array,gap);
        }
    }

    /**
     * 对每组进行排序
     * 这段代码其实跟插入排序差不多,就是i其实位置在gap上,j每次递减递增gap个单位
     * @param array
     * @param gap
     */

    public static 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;
        }
    }

时间复杂度?

稳定性?不稳定,因为分组交换之后会打乱相同的数字原本的前后顺序

编写个代码来测试一下两者的运行时间

import java.util.Arrays;

public class Test {
    public static void testInsert(int[] array){
        int[] tmpArray = Arrays.copyOf(array, array.length);
        long startTime = System.currentTimeMillis();
        Sort.insertSort(tmpArray);
        long endTime = System.currentTimeMillis();
        System.out.println("直接插入排序时间:"+(endTime-startTime));
    }
    public static void testShell(int[] array){
        int[] tmpArray = Arrays.copyOf(array, array.length);
        long startTime = System.currentTimeMillis();
        Sort.shellSort(tmpArray);
        long endTime = System.currentTimeMillis();
        System.out.println("希尔排序时间:"+(endTime-startTime));
    }
    public static void initArray(int[] array){
        for (int i = 0; i < array.length; i++) {
            array[i] = array.length-i;
        }
    }

    public static void main(String[] args) {
        int[] array = new int[10_0000];
        initArray(array);
        testInsert(array);
        testShell(array);
    }
}


选择排序

直接选择排序

动图如下:
在这里插入图片描述

设置i和j,遍历当前i下标后面的元素(j++),找到一个最小的值与i下标的元素进行替换

然后i++,进行下一个元素的交换

    /**
     * 选择排序
     * 时间复杂度O(n^2)
     * 空间复杂度O(1)
     * 稳定性:不稳定
     * @param array
     * @param i
     * @param j
     */
    public static void swap(int[] array,int i,int j){
        int tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }
    public static void selectSort(int[] array){
        for (int i = 0; i < array.length; i++) {
            int minIndex = i;
            //j往后遍历,每次找到比minIndex下标元素小的就进行下标替换
            for (int j = i+1; j < array.length; j++) {
                if(array[j] < array[minIndex]){
                    minIndex = j;
                }
            }
            swap(array,i,minIndex);
        }
    }

双向选择排序

设置最左边下标l和最右边下标r,设置i = l+1,往后遍历,找到最小值的下标和最大值的下标

接着把minIndex的元素与l交换,maxIndex的元素与r交换

接着再i++,l++,r--,重复上面的步骤

注意:如果l的位置就是最大值,经过与最小值交换之后不一定有序

所以我们要把minIndex和maxIndex换过来

    public static void selectSort2(int[] array){
        int left = 0;
        int right = array.length-1;
        while(left<right){
            int minIndex = left;
            int maxIndex = right;
            //找到最大值和最小值下标
            for (int i = left+1; i <= right; i++) {
                if(array[i] < array[minIndex]){
                    minIndex = i;
                }
                if(array[i] > array[maxIndex]){
                    maxIndex = i;
                }
            }
            swap(array,minIndex,left);
            if(maxIndex == left){
                maxIndex = minIndex;
            }
            swap(array,maxIndex,right);
            left++;
            right--;
        }
    }

堆排序

可以看着我这篇博客的思路数据结构:优先级队列(堆)-CSDN博客

代码如下:

    private static void createHeap(int[] array) {
        for (int parent = (array.length-1-1)/2; parent >= 0 ; parent--) {
            siftDown(array,parent,array.length);//alt+enter
        }
    }

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

    /**
     * 时间复杂度:O(N*logN)
     * 空间复杂度:O(1)
     * 稳定性:不稳定的排序
     * @param array
     */
    public static void heapSort(int[] array) {
        createHeap(array);
        int end = array.length-1;
        while (end > 0) {
            swap(array,0,end);
            siftDown(array,0,end);
            end--;
        }
    }

交换排序

冒泡排序

动图如下:
在这里插入图片描述

    /**
     * 时间复杂度:O(n^2)
     * 空间复杂度:O(1)
     * 稳定性:稳定
     * @param array
     */
    public static void bubbleSort(int[] array){
        //i代表趟数
        for (int i = 0; i < array.length-1; i++) {
            //每一趟都比较上一趟有没有交换
            boolean flg = false;
            //j来比较每个数据的大小
            for (int j = 0; j < array.length-1-i; j++) {
                if(array[j]>array[j+1]){
                    swap(array,j,j+1);
                    flg = true;
                }
            }
            if(flg==false){
                break;
            }
        }
    }

快速排序

Hoare法

单趟动图如下:
在这里插入图片描述

第一轮交换之后,6在中间,6的左边都比6小,右边都比6大

第二轮和第一轮一样,接着不停地递归下去

这些数组可以拆分并组成一棵二叉树如下图,二叉树就是左边和右边分别递归 

   public static void quickSort(int[] array){
        quick(array,0,array.length-1);
    }

    private static 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);
    }

接下来我们要来搞定partition的方法,也就是要找到整个序列中间值

先确定第一个元素是pivot元素

right指针负责找到比array[right]小的数字,left指针负责找到比array[left]大的数字

找到了就进行交换,直到左右指针相遇

    private static int partitionHoare(int[] array, int left, int right){
        int tmp = array[left];
        int i = left;
        //整个的循环,要求left和right相遇之后能交换数字
        while(left<right){
            //单独的循环,因为如果right--一直找不到比tmp大的数,而right不能一直减到最左边的边界
            //所以需要再规定依次left<right
            while(left<right && array[right] >= tmp){
                right--;
            }
            while (left<right && array[left] <= tmp){
                left++;
            }
            swap(array,left,right);
        }
        swap(array,i,left);
        return left;
    }

思考:

1.为什么这里要有一个等于号

如果不用=号可能会进入死循环

如果没有等于号,第一个循环因为6不大于6,right没办法--,同理left没办法++,走到后面right和left进行交换,只是相当于6和6这两个元素的下标进行交换而已,整个数组也没有进行排序

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

如果先走左边,有可能会出现相遇的时大的数据,最后把大的数据放在最前面

而先走右边的话可以先遇到小的,可以把小的放到前面


时间复杂度?

最好情况:

根据上方代码的递归分治思想,找到中间值,递归左边递归右边再出中间值,每次出一个中间值都是把一部分一分为二进行寻找(logn)

每次循环都有n个元素要进行遍历

最后总的时间复杂度O(n*logn)

最坏情况:

比如:1 2 3 4 5

递归时right每次找到比1小的值都要遍历n个元素,在循环中每次都要遍历n个元素检查是否进行交换

所以总的时间复杂度就是O(n^2)

⚠快排使用时,数据一般不是逆序或者有序的

⚠快排使用时,往往会有栈的溢出

这个错误跟我们递归的层次有关系,递归越多在栈上开辟的空间就越多,而IDEA默认给出的空间是256KB,这么小的空间容纳不了我们10万个数据进行栈空间的开辟

我们要对IDEA进行一个调整


挖坑法

单趟动图如下:
在这里插入图片描述

    private static int partitionHole(int[] array, int left, int right){
        int tmp = array[left];
        //整个的循环,要求left和right相遇之后能交换数字
        while(left<right){
            //单独的循环,因为如果right--一直找不到比tmp大的数,而right不能一直减到最左边的边界
            //所以需要再规定依次left<right
            while(left<right && array[right] >= tmp){
                right--;
            }
            array[left] = array[right];
            while (left<right && array[left] <= tmp){
                left++;
            }
            array[right] = array[left];
            swap(array,left,right);
        }
        array[left] = tmp;
        return left;
    }

前后指针法

思路:
1、选出一个key,一般是最左边或是最右边的。
2、起始时,prev指针指向序列开头,cur指针指向prev+1。
3、若cur指向的内容小于key,则prev先向后移动一位,然后交换prev和cur指针指向的内容,然后cur指针++;若cur指向的内容大于key,则cur指针直接++。如此进行下去,直到cur到达end位置,此时将key和++prev指针指向的内容交换即可。

  /**
     * 前后指针法:
     *  总结:
     *  1. Hoare 优先级: 2
     *  2. 挖坑法 优先级:1
     *  3. 前后指针法 优先级:3
     *  这3种方式  每次划分之后的前后顺序 有可能是不一样的
     * @param array
     * @param left
     * @param right
     * @return
     */
    private static int partition(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;
    }

选择题: 

对记录( 54,38,96,23,15,72,60,45,83 )进行从小到大的直接插入排序时,当把第 8 个记录 45 插入到有序表时,为找到插入位置需比较() 次?(采用从后往前比较)
A: 3 B: 4 C: 5 D: 6

这道题不要太死板地做,题目问我们第8个记录45,那么说明45之前的数字一定是有序的

那45就需要比较到比它小的数就行,很简单选C


快排的优化

上面我们提到了,快排一旦递归较多的时候容易出现栈溢出的情况

所以我们优化方向:1.减少递归次数;2.让每一次都能均匀地分割数组

三数取中法

上面提到当快排的hoare和挖坑法遇到有序数列时,l和r都跑到第一个元素去,右边有一大坨数字,无法实现取到中间数的效果

我们采用三数取中法

1.先找到数列中间下标(m)的数字

int mid = (left+right)/2;

定义大前提

 

2.找出l,m,r下标的三个数字排在最中间的那个数

array[left]<array[right]

array[left]>array[right]

 

        if(array[left] < array[right]){
            if(array[mid]<array[left]){
                return left;
            }else if(array[mid] > array[right]){
                return right;
            }else{
                return mid;
            }
        }else{
            //array[left] > array[right]
            if(array[mid]>array[left]){
                return left;
            }else if(array[mid] < array[right]){
                return right;
            }else{
                return mid;
            }
        }

3.把m坐标元素与l坐标元素交换

4.然后以4为基准,利用r--找到比4小的元素,把这个元素与4交换

这样就不会出现找不到左数或者右数的情况

5.接着左右子树进行遍历就行了

        if(start>=end){
            return;
        }
        //1 2 3 4 5 6 7
        int index = middleNum(array,start,end);
        swap(array,index,start);
        //4 2 3 1 5 6 7
        int pivot = partition(array,start,end);
        quick2(array,start,pivot-1);
        quick2(array,pivot+1,end);

6.排序的最终步骤一般集中在数组二叉树的最后两层,而当排序到这个地方的时候,整个数组已经偏向有序的状态了,所以我们没必要再让二叉树继续递归下去,我们可以采用插入排序,在一个很小的序列中进行排序。这样可以降低树的高度,减少递归的次数

整个的代码

    private static int middleNum(int[] array, int left,int right){
        int mid = (left+right)/2;
        //求中位数的下标
        if(array[left] < array[right]){
            if(array[mid]<array[left]){
                return left;
            }else if(array[mid] > array[right]){
                return right;
            }else{
                return mid;
            }
        }else{
            //array[left] > array[right]
            if(array[mid]>array[left]){
                return left;
            }else if(array[mid] < array[right]){
                return right;
            }else{
                return mid;
            }
        }
    }

    public static void insertSort(int[] array, int left,int right) {
        for (int i = left+1; i <= right; i++) {
            int tmp = array[i];
            int j = i - 1;
            for (; j >= left; j--) {
                if (array[j] > tmp) {
                    array[j + 1] = array[j];
                } else {
                    break;
                }
            }
            array[j + 1] = tmp;
        }
    }
    private static void quick2(int[] array, int start, int end){
        if(start>=end){
            return;
        }
        if(end-start+1<=15){
            insertSort(array,start,end);
            return;
        }
        //1 2 3 4 5 6 7
        int index = middleNum(array,start,end);
        swap(array,index,start);
        //4 2 3 1 5 6 7
        int pivot = partition(array,start,end);
        quick2(array,start,pivot-1);
        quick2(array,pivot+1,end);
    }

非递归快排

先利用挖坑法排好序

创建一个栈,把6左边第一个和最后一个元素(pivot-1)的位置放入栈中,右边同理

弹出一个9给r,弹出6给l

 再重复一次挖坑法partition方法

9右边的元素不需要递归,所以直接当成pivot 

把9左边的第一个元素下标(6)和最后一个元素(7)放入栈中

 

怎么判断pivot左边和右边有多少个元素呢?

当pivot+1 = e的时候,右边只有1个元素;当pivot+1<e的时候,右边有两个或两个以上的元素

相反当pivot-1>s,左边有两个或两个以上元素

总结步骤:

1.调用partition方法找出整个数组的中间数位置pivot

2.左边有没有两个元素,下标放到栈

3.右边一样

4.判断栈空不空-->不空的话就pop两个元素出来分别交给r和l(注意最先出来的给r,慢一点的给l)

整个代码:

    public static void quickSortNor(int[] array){
        int start = 0;
        int end = array.length-1;
        Stack<Integer> stack = new Stack<>();
        int pivot = partitionHoare(array,start,end);
        if(pivot>start+1){
            stack.push(start);
            stack.push(pivot-1);
        }
        if(pivot+1<end){
            stack.push(pivot+1);
            stack.push(end);
        }
        while(!stack.isEmpty()){
            end = stack.pop();
            start=stack.pop();
            pivot = partitionHoare(array,start,end);
            if(pivot>start+1){
                stack.push(start);
                stack.push(pivot-1);
            }
            if(pivot+1<end){
                stack.push(pivot+1);
                stack.push(end);
            }
        }
    }

归并排序

分治算法+二路归并

分解:

    public static void mergeSort(int[] array){
        mergeSortFun(array,0, array.length-1);
    }

    private static void mergeSortFun(int[] array,int start,int end){
        if(start>=end){
            return;
        }
        //分解
        int mid = (start+end)/2;
        mergeSortFun(array,start,mid);
        mergeSortFun(array,mid+1,end);
        //合并
        merge(array,start,mid,end);
    }

归并方法

创建一个tmpArr数组记录排序好的数字

先进行s1和s2两个元素的比较,s2的元素比较小先扔到tmpArr里面,s2++

 接着再比较s2和s1,发现s1更小,扔到tmpArr里面,s1++

后面的步骤差不多,比较s1和s2两个元素,谁小谁放进数组

    private static void merge(int[] array, int left, int mid,int right) {
        int s1 = left;
        int e1 = mid;
        int s2 = mid+1;
        int e2 = right;
        int[] tmpArr = new int[right-left+1];
        int k = 0;//tmpArr的下标
        //同时满足两个归并段都有数据
        while(s1 <= e1 && s2 <= e2){
            if(array[s1] <= array[s2]){
                tmpArr[k++] = array[s1++];
            }else{
                tmpArr[k++] = array[s2++];
            }
        }
        while(s1 <= e1){
            tmpArr[k++] = array[s1++];
        }
        while(s2 <= e2){
            tmpArr[k++] = array[s2++];
        }
        //把排好的数据拷贝回原来的数组array中
        for (int i = 0; i < tmpArr.length; i++) {
            array[i+left] = tmpArr[i];
        }
    }

时间复杂度

每次都要遍历n个元素,而分解过程分治算法和二路归并的复杂度logn

总的复杂度O(N*logN)

稳定性:稳定


非递归归并

先让每组的两个数据进行排序,接着再让两个组的四个数据进行排序

每组一个数据进行排序

两组的数据排序

 

        int gap = 1;
        while(gap<array.length){
            for (int i = 0; i < array.length; i = i+gap*2) {
                int left = i;
                int mid = left+gap-1;
                int right = mid+gap;

                merge(array,left,mid,right);
            }
            gap*=2;
        }

mid和right的定义方式可能会有越界的风险

所以我们需要进行风险修正

                if(mid >= array.length){
                    mid = array.length-1;
                }
                if(right>=array.length){
                    right = array.length-1;
                }

整个代码:

    public static void mergeSortNor(int[] array){
        int gap = 1;
        while(gap<array.length){
            for (int i = 0; i < array.length; i = i+gap*2) {
                int left = i;
                int mid = left+gap-1;
                int right = mid+gap;
                if(mid >= array.length){
                    mid = array.length-1;
                }
                if(right>=array.length){
                    right = array.length-1;
                }
                merge(array,left,mid,right);
            }
            gap*=2;
        }
    }

应用

前提:内存只有 1G ,需要排序的数据有 100G
因为内存中因为无法把所有数据全部放下,所以需要外部排序,而归并排序是最常用的外部排序

1.先把文件切割成200份,每个512M

2.分别对512M排序,任意排序都行,因为内存放的下

3.进行2路归并,同时对200份有序文件做归并过程,最终结果就是有序了


排序总结


其他排序

计数排序

简单版本

有这么一组数字,要求你进行排序,注意这组数组在0~9之间

1.先申请一个计数数组count,设置一个i,定义co把每个数字出现的次数记录下来。i遍历上面的数字,每次遍历到重复的数字就co++(count[array[i] - minVal]++)

⚠计数数组上面的数字代表数组里面出现的数字,不是下标

如果不一定是以0为最小值的呢?

那我们就需要定义数组最小值minVal和最大值maxVal了

        int minVal = array[0];
        int maxVal = array[0];
        for (int i = 0; i < array.length; i++) {
            if(array[i]<minVal){
                minVal = array[i];
            }
            if(array[i]>maxVal){
                maxVal=array[i];
            }
        }

数组长度?

        //确定计数数组的长度
        int len = maxVal-minVal+1;
        int[] count = new int[len];
        //遍历array数组 把数据出现的次数存储到计数数组中
        for (int i = 0; i < array.length; i++) {
            count[array[i]-minVal]++;
        }

2.设置一个i遍历这个计数数组,把每个数字重复(如果有)记录下来并写回原来的数组

 

        //遍历计数数组,把实际的数组写回array数组
        int index = 0;
        for (int i = 0; i < count.length; i++) {
            while(count[i]>0){
                //这里需要写回array,得从array的0位置开始写
                array[index] = i+minVal;
                index++;
                //每次写进array一个元素,计数数组的对应元素数量就得减少
                count[i]--;
            }
        }

整个代码:

    public static void countSort(int[] array){
        //求数组最大值和最小值  O(N)
        int minVal = array[0];
        int maxVal = array[0];
        for (int i = 0; i < array.length; i++) {
            if(array[i]<minVal){
                minVal = array[i];
            }
            if(array[i]>maxVal){
                maxVal=array[i];
            }
        }
        //确定计数数组的长度
        int len = maxVal-minVal+1;
        int[] count = new int[len];

        //遍历array数组 把数据出现的次数存储到计数数组中    O(N)
        for (int i = 0; i < array.length; i++) {
            count[array[i]-minVal]++;
        }
        //遍历计数数组,把实际的数组写回array数组 
        //跟最大值和最小值有关系,所以是O(范围)
        int index = 0;
        for (int i = 0; i < count.length; i++) {
            while(count[i]>0){
                //这里需要写回array,得从array的0位置开始写
                array[index] = i+minVal;
                index++;
                //每次写进array一个元素,计数数组的对应元素数量就得减少
                count[i]--;
            }
        }
    }

时间复杂度:O(MAX(N, 范围))

空间复杂度:O(范围)

稳定性:不稳定


复杂版本(稳定版本)

这里字数太多(主要是我懒~),大家可以去看这篇博客计数排序 - 知乎 (zhihu.com)


基数排序 

基数排序动图演示

从低位到高位数字,让每位数字依次有序

代码见下:

1.10 基数排序 | 菜鸟教程 (runoob.com) 


桶排序 

【排序】图解桶排序_桶排序图解-CSDN博客

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值