常见排序算法的实现

排序是数据结构中比较重要的一个算法,也是笔试面试中常考的算法题,本篇文章介绍内部排序,也就是数据全在内存中处理的排序。
主要有插入排序(直接插入排序/希尔排序),选择排序(直接选择排序/堆排序),交换排序(冒泡排序/快速排序),归并排序等。

1、插入排序

1.1 直接插入排序

1.1.1 基本思想
直接插入排序是一种简单的插入排序算法,就像我们平时玩的扑克牌,拿第一张牌的时候,已经是有序的,后面拿到的每一张牌,要和前面已经有序的牌进行比较,在有序部分找一个合适的位置,把拿到的牌放进去。
那么什么就是合适的位置呢?我们可以进行遍历查找,从后往前用拿到的牌和有序部分进行比较,比它大就放右边,比它小就放左边。
在这里插入图片描述
上图中蓝色部分表示已经有序,橙色部分代表要即将要进行比较的数据。
第一次拿9去比较,此时数组中还没有数据,所以直接将9放入蓝色部分,代表已经有序。
在这里插入图片描述
第二次用3去比较,因为a[i]<a[j],所以 j - -,j+1的位置就是要插入的位置。
在这里插入图片描述
第三次用1比较,同样的道理,有序部分一直增加:
在这里插入图片描述
第四次用5去比较,如果a[i]>a[j],就代表找到了合适的位置,j+1的位置依然是要插入的位置,后面的数据以此类推。
在这里插入图片描述
如果遇到a[i] == a[j],为了保证稳定性,我们也认为找到了合适的位置,j不用再往前移。
那么如何把橙色的数插入到合适的下标处呢,其实这就相当于一个顺序表,在给定位置pos处做插入。

1.1.2 算法实现

public static void insertSort(int[] array) {
        for (int i = 0; i < array.length; i++) {
            //有序区间[0,i)
            //无序区间[i,array.length)

            //1、在有序区间遍历查找,从后往前
            int j;
            for (j = i - 1; j >= 0 && array[i] < array[j]; j--) {

            }

            //j+1 就是要插入数据的下标
            //2、插入数据,从后往前搬移数据
            int pos = j + 1;
            int key = array[i];
            for (int k = i; k > pos; k--) {
                array[k] = array[k - 1];
            }

            array[pos] = key;
        }
    }

可以采用边查找边搬移数据对代码进行简化。

public static void insertSort2(int[] array){
        for (int i = 0;i<array.length;i++){
            int key = array[i];
            int j = i-1;
            //边查找边搬移数据
            for (;j>=0 && key < array[j+1];j--){
                array[j+1] = array[j];
            }
            array[j+1] = key;
        }
    }

上面的是遍历查找的方法,下面这段代码是采用二分查找的方法,搬移数据的过程都是一样的。

public static void insertSort3(int[] array) {
        for (int i = 0; i < array.length; i++) {
            int key = array[i];
            //有序[0,i)
            int left = 0;
            int right = i;

            //二分查找
            while (left < right) {
                int mid = left + (right - left) / 2;
                if (key == array[mid]) {
                    left = mid + 1;
                } else if (key < array[mid]) {
                    right = mid;
                } else {
                    left = mid + 1;
                }
            }

            //搬数据
            int pos = left;
            for (int k=i;k>pos;k--){
                array[k] = array[k-1];
            }
            array[pos] = key;
        }
    }

1.1.3 算法分析

  • 时间复杂度:
    最好:O(n)
    最坏:O(n^2)
    平均:O(n^2)
  • 空间复杂度:O(1)
  • 稳定性:稳定

1.2 希尔排序

1.2.1 基本思想
希尔排序实际上是对直接插入排序的优化。首先我们对一组数据进行预排序,目的是很快的让数据基本有序,然后再分组进行插排,这样的话速度很快。
那么会有一个问题,分组到底多大才合适呢?分组分的越多,数据往后走的步伐越大,而分组分的越少,排完后越接近有序,这样看来各有优点,所以我们可以做多次分组插排,分组的个数从大到小,整个过程都是不断进行预排和插排。
1.2.2 算法实现

private static void insertSortWithGap(int[] array, int gap) {
        for (int i = 0; i < array.length; i++) {
            int key = array[i];
            int j = i - gap;
            for (; j >= 0 && key < array[j + 1]; j = j - gap) {
                array[j + gap] = array[j];
            }
            array[j + gap] = key;
        }
    }
    
    public static void shellSort(int[] array) {
        int gap = array.length;
        while (true) {
            //gap = gap/2
            gap = (gap / 3) + 1;
            insertSortWithGap(array,gap);
            if (gap==1){
                break;
            }
        }
    }

1.2.3 算法分析

  • 时间复杂度
    最好:O(n)
    最坏:O(n^2) 概率变小
    平均:O(n^1.2-1.3)
  • 空间复杂度:O(1)
  • 稳定性:不稳定(很难保证相同的数据分到一个组里)

2、选择排序

2.1 直接选择排序

2.1.1 基本思想
遍历数组,每次找出最小的一个数放在放在无序部分的最开始位置,然后对剩下的数再进行遍历。需要注意的是,遍历的过程中不需要交换元素,一次遍历结束找到最小值,将最小值进行交换。有序部分的下标是[0,i),无序部分的下标是[i,length)。
在这里插入图片描述
2.1.2 算法实现

public static void selectSort(int[] array) {
        //每次选择一个最小的放到无序部分的最开始位置
        for (int i = 0; i < array.length; i++) {
            //有序[0,i)
            //无序[i,array.length)
            int min = i;    //记录最终最小数所在的下标
            for (int j = i + 1; j < array.length; j++) {
                if (array[j] < array[min]) {
                    min = j;
                }
            }

            //最终交换一次
            swap(array, min, i);
        }
    }

2.1.3 算法分析

  • 时间复杂度
    最好:O(n^2)
    最坏:O(n^2)
    平均:O(n^2)
  • 空间复杂度:O(1)
  • 稳定性:不稳定

2.2 堆排序

2.2.1 基本思想
堆排序首先需要将无序数组里的元素构建成一个大堆,然后将堆顶元素与末尾元素进行交换,这样一来最大的元素就到了数组末端。重新调整结构,使它满足堆的定义,继续交换堆顶元素和当前末尾元素(当前末尾元素是除刚刚已经交换过的最大元素之外的最后一个元素),反复执行调整和交换的步骤,直到整个数组有序。具体来说就是:
在这里插入图片描述
建大堆:
从最后一个非叶子节点开始,从左至右,从下至上进行调整。
在这里插入图片描述
找到第二个非叶子节点4,再进行调整,使其满足大堆的性质。
在这里插入图片描述
这时无序序列已经构成了一个大堆,将堆顶元素与末尾元素进行交换,使末尾元素最大。
在这里插入图片描述
然后继续调整堆,再将堆顶元素与新的末尾元素交换,得到第二大元素,如此反复交换,调整。
在这里插入图片描述
最终得到的有序序列为:
在这里插入图片描述

2.2.2 算法实现

private static void heapify(int[] array, int size, int index) {
        //1、判断index是不是叶子
        while (2 * index + 1 < size) {
            //2、找到最大的孩子的下标
            int max = 2 * index + 1;
            if (max + 1 < size && array[max + 1] > array[max]) {
                max = 2 * index + 2;
            }

            //3、判断最大的孩子和根的值
            if (array[index] < array[max]) {
                swap(array, index, max);
                index = max;
            } else {
                //4、根的值比较大,可以直接结束了
                //不交换,也不继续往下走了
                break;
            }
        }
    }

    private static void createHeap(int[] array) {
        //[最后一个非叶子节点的下标,根] 向下调整
        //[(array.length-2)/2,0]
        for (int i = (array.length - 2) / 2; i >= 0; i--) {
            heapify(array, array.length, i);
        }
    }

    public static void heapSort(int[] array) {
        //建堆 建大堆
        createHeap(array);
        //减治处理
        for (int i = 0; i < array.length; i++) {
            //无序[0,length-i-1)
            //有序[length-i,length)
            //最大的数在[0],最大的数应该放到的下标是[length-1-i]
            //交换最大的数到无序部分最后的位置
            swap(array, 0, array.length - 1 - i);
            //处理[0],无序剩余部分满足堆的性质
            //无序[0,lenght-i-2)
            //有序[lenght-i-1,length)
            //size  剩余无序部分的长度
            //无序部分,除了[0],都满足堆的性质
            heapify(array, array.length - 1 - i, 0);
        }
    }

2.2.3 算法分析

  • 时间复杂度
    最好:O(nlog2n)
    最坏: O(nlog2n)
    平均:O(nlog2n)
  • 空间复杂度:O(1)
  • 稳定性:不稳定

3、交换排序

3.1 冒泡排序

3.1.1 基本思想
冒泡排序是最基础的排序,每次对相邻的两个元素进行操作,升序如果后一个数比前一个数小,就要进行交换。对代码进行优化可以加一个标志位isSorted=true,如果一趟冒泡结束,isSorted仍为true,说明数组已经有序,就可以不用再进行比较了。
3.1.2 算法实现

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

3.1.3 算法分析

  • 时间复杂度
    最好:O(n)
    最坏:O(n^2)
    平均:O(n^2)
  • 空间复杂度:O(1)
  • 稳定性:稳定

3.2 快速排序

3.2.1 基本思想
快速排序分三步进行。
1、首先选择一个基准值,这里我们选择区间最右边的数作为基准值。
在这里插入图片描述
2、遍历整个区间,每个数都和基准值作比较,并且发生一定交换,遍历结束后,使得比基准值小的数(包括等于)都在基准值的左边,比基准值大的数(包括等于)都在基准值的右边。
在这里插入图片描述
3、采用分治算法,分别对左右两个小区间进行同样方式的处理,直到小区间的size == 0或者size == 1就代表已经有序了。
那么如何让小的都在基准值的左边,大的都在基准值右边呢?有三种方法。
1)Hover法
2)挖坑法
3)前后下标法
Hover法就是定义下图中这样的四个引用,begin引用一直往右走,end引用一直往左走,过程中保证begin左边的值始终比基准值小,end右边的的值始终比基准值大,如果不符合就交换begin和end的值后继续,直到begin引用和end引用相遇,这样就把区间分成了三段,最后再将基准值和begin的位置对应的值进行交换,就把基准值放到了中间:
在这里插入图片描述
挖坑法和Hover法的思路基本一样,不过挖坑法不用交换begin和end,它是提前将pivot位置对应的值保存下来,然后将pivot位置变成一个坑,begin开始往右走,遇到比pivot值大的停下来,放到坑的那个位置,然后end开始开始往左走,遇到比pivot值小的停下来,放到新的坑的位置,接着begin再往右走,一直循环,直到begin和end相等,把pivot放到坑的位置。
以下图为例:
在这里插入图片描述
先把5的位置变为坑,begin往右走,9比5大,把9放到坑的位置,原来9的位置变成新的坑:
在这里插入图片描述
然后end往左走,3比5小,把3放到坑的位置,原来3的位置变成新的坑:
在这里插入图片描述
接着begin继续往右走,走到8的位置比pivot值大,把8放到坑的位置,原来8的位置变成新的坑:
在这里插入图片描述
一直重复,直到begin和end相遇,把pivot的值放入坑的位置:
在这里插入图片描述
前后下标法是定义两个引用 i 和 j ,遍历整个数组,如果 i 对应的值大于等于pivot的值, i 往右走 j 不走,如果 i 对应的值小于 pivot的值,交换 i 和 j 对应的值,然后 i 和 j 都往右走一步,保证 j 左边的值都比pivot值小, j 和 i 之间的值都比pivot值大,一直重复上述操作,直到遍历结束。
以下图为例:
在这里插入图片描述
6大于基准值5,i 往右走 d 不走:
在这里插入图片描述
3小于基准值5,交换3和6后 i 和 j 都往右走一步:
在这里插入图片描述
以此类推,直到遍历结束:
在这里插入图片描述
3.2.2 算法实现
Hover法:

private static int parition1(int[] array,int left,int right){
        int begin = left;
        int end = right;
        int pivot = array[right];
        while (begin<end){
            while (begin<end && array[begin]<=pivot){
                begin++;
            }
            //array[begin]>pivot
            while (begin<end && array[end]>=pivot){
                end--;
            }
            //array[end]<pivot
            swap(array,begin,end);
        }
        //注意不要交换pivot的位置,pivot只是一个局部变量
        swap(array,begin,right);
        //返回基准值的位置
        return begin;
    }

    private static void quickSortInner(int[] array,int left,int right){
        //直到size==1 || size==0
        if (left==right){
            //size==1
            return;
        }
        if (left>right){
            //size==0
            return;
        }
        //要排序的区间是array[left,right]
        //1、找基准值,array[right]
        //2、遍历整个区间,把区间分成三部分
        int pivotIndex = parition1(array,left,right);
        //比基准值小的[left,pivotIndex-1]
        //比基准值大的[pivotIndex+1,right]
        //3、分治算法
        //处理比基准值小的区间
        quickSortInner(array,left,pivotIndex-1);
        //处理比基准值大的区间
        quickSortInner(array,pivotIndex+1,right);
    }

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

挖坑法:

private static int parition2(int[] array,int left,int right){
        int begin = left;
        int end = right;
        int pivot = array[right];
        while (begin<end){
            while (begin<end && array[begin]<=pivot){
                begin++;
            }
            array[end] = array[begin];

            while (begin<end && array[end]>=pivot){
                end--;
            }
            array[begin] = array[end];
            
        }
        array[begin] = pivot;
        //返回基准值的位置
        return begin;
    }

    private static void quickSortInner(int[] array,int left,int right){
        //直到size==1 || size==0
        if (left==right){
            //size==1
            return;
        }
        if (left>right){
            //size==0
            return;
        }
        //要排序的区间是array[left,right]
        //1、找基准值,array[right]
        //2、遍历整个区间,把区间分成三部分
        int pivotIndex = parition1(array,left,right);
        //比基准值小的[left,pivotIndex-1]
        //比基准值大的[pivotIndex+1,right]
        //3、分治算法
        //处理比基准值小的区间
        quickSortInner(array,left,pivotIndex-1);
        //处理比基准值大的区间
        quickSortInner(array,pivotIndex+1,right);
    }

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

前后下标法:

private static int parition3(int[] array,int left,int right){
        int j = left;
        for (int i=left;i<right;i++){
            if (array[i] < array[right]){
                swap(array,j,i);
                j++;
            }
        }
        swap(array,j,right);
        return j;
    }

    private static void quickSortInner(int[] array,int left,int right){
        //直到size==1 || size==0
        if (left==right){
            //size==1
            return;
        }
        if (left>right){
            //size==0
            return;
        }
        //要排序的区间是array[left,right]
        //1、找基准值,array[right]
        //2、遍历整个区间,把区间分成三部分
        int pivotIndex = parition1(array,left,right);
        //比基准值小的[left,pivotIndex-1]
        //比基准值大的[pivotIndex+1,right]
        //3、分治算法
        //处理比基准值小的区间
        quickSortInner(array,left,pivotIndex-1);
        //处理比基准值大的区间
        quickSortInner(array,pivotIndex+1,right);
    }

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

3.2.3 算法分析

  • 时间复杂度
    最好:O(nlog(n))
    最坏:O(n^2) 选最边上作为基准值时,当数组已经有序或者数组逆序,都是最坏的情况
    平均:O(n
    log(n))
  • 空间复杂度:
    最好:O(log n)
    最坏:O(n)
    平均:O(log n)
  • 稳定性:不稳定

补充:
如何避免最坏的情况,也就是单支树的情况呢,有两种方法可以解决:
1)随机法
顾名思义就是在数组中随机选取一个数作为基准值(random.nextInt())
2)三数取中法
在数组中选取最左边,最中间和最右边的三个数,比较大小,找到三个数中中间的数,比如说9 8 7 6 5 ,选取9 7 5,发现7是中间的数,那么7就是基准值。
利用上面两种方法选出来的基准值不在最边上,我们可以先把基准值交换到最边上再按之前的方法进行排序。

4、归并排序

4.1.1 基本思想
归并排序和快速排序唯一的相同点是都采用了分治算法的思想,其他的完全不一样。归并排序首先把要排序的区间平均分割成两部分,然后采用分治算法,对左右两个区间进行同样方式的排序,直到size == 1,表示区间已经有序,size == 0,表示区间没有数了(实际不会走到),最后再合并左右两个区间到一个有序区间,这一步需要开辟额外的空间。
以下图为例:
在这里插入图片描述
递归的过程如下:
在这里插入图片描述
橙色部分表示已经有序的区间,剩下的递归过程以此类推,递归结束就可以得到一个有序数组。
4.1.2 算法实现

private static void merge(int[] array, int low, int mid, int high) {
        int[] extra = new int[high - low];
        int i = low;    //遍历array[low,mid)
        int j = mid;    //遍历array[mid,high)
        int x = 0;      //遍历extra

        while (i<mid && j<high){
            if (array[i] <= array[j]){
                extra[x++] = array[i++];
            }else {
                extra[x++] = array[j++];
            }
        }

        while (i<mid){
            extra[x++] = array[i++];
        }

        while (j<high){
            extra[x++] = array[j++];
        }

        //把数据从额外的空间搬回原来的空间,k-low是因为额外空间下标是从0开始,原空间下标从low开始
        for (int k=low;k<high;k++){
            array[k] = extra[k-low];
        }
    }

    private static void mergeSortInner(int[] array, int low, int high) {
        //array[low,high)
        //[3,4)
        if (low == high - 1) {
            return;
        }
        if (low >= high) {
            return;
        }

        //1、平均切分
        int mid = low + (high - low) / 2;
        //[low,mid)+[mid,high)
        //2、分治算法处理所有两个小区间
        mergeSortInner(array, low, mid);
        mergeSortInner(array, mid, high);

        //左右两个小区间已经有序了
        merge(array, low, mid, high);
    }

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

对代码进行优化:
由于归并的时候每次都要开辟额外的空间,所以我们可以提前申请一个最大的额外空间,每次归并的时候带着这个额外空间,这样可以避免造成大量的内存碎片。

public static void mergeSort(int[] array) {
        int[] extra = new int[array.length];
        mergeSortInner(array, 0, array.length, extra);
    }

补充:归并排序非递归版
归并排序递归版一开始的目的是拆成一个数一个数进行归并,那么既然本来就要拆成一个数一个数归并,那么我们可以不进行递归,直接一个数一个数进行归并,以下图为例:
在这里插入图片描述
接下来3,9和2,5一组进行归并,4,1和7一组进行归并:
在这里插入图片描述
然后2,3,5,9和1,4,7一组进行归并:
在这里插入图片描述
具体实现代码如下:

//归并排序(非递归)
    public static void mergeSortNoR(int[] array){
        int[] extra = new int[array.length];

        //外层循环控制循环的次数,内层循环控制分组
        for (int i=1;i<array.length;i*=2){
            for (int j=0;j<array.length;j+=2*i){
                int low = j;
                int mid = low +i;
                if (mid>=array.length){
                    continue;
                }
                int high = mid + i;
                if (high>array.length){
                    high=array.length;
                }

                merge(array,low,mid,high,extra);
            }
        }
    }

4.1.3 算法分析

  • 时间复杂度:O(n*log(n))
  • 空间复杂度:O(1)
  • 稳定性:稳定
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值