八大排序算法(Java)

38 阅读5分钟

时间复杂度及稳定性

一、插入排序

将每一步待排序元素插入到已经完成排序的元素中,直到插完所有元素为止。

具体的代码实现:

public class InsertSort implements SortMethod{
    @Override
    public int[] sort(int[] nums) {
        for (int i = 0; i < nums.length; i++) { //遍历每一个元素
            for (int j = i; j > 0 ; j--) {  // 与前面的元素相比较
                if (nums[j]<nums[j-1]){
                    swap(nums,j,j-1);
                }else { //已经有序了
                    break;
                }
            }
        }
        return nums;
    }
}

复杂度分析:

时间复杂度为O(n2),空间复杂度为O(1),若元素越接近有序,则插入排序的时间效率越高。插入排序是一种稳定的排序算法

二、希尔排序

希尔排序也叫缩小增量排序,本质上是对插入排序的优化(利用插入排序当元素集合越接近有序,插入排序算法效率更高的特点)。

思想

对待排序数组中的元素进行分组, 从第一个元素开始,按照数组下标中间隔为gap大小的元素分为一组,对每一组进行排序,重新选择gap的大小使得原始数据更加有序,当gap=1的时候就是插入排序。

public class ShellSort implements SortMethod{
    @Override
    public int[] sort(int[] nums) {
        int gap = nums.length;
        while (gap > 1){
            gap = gap/3 +1; //调整gap的大小,当gap=1的时候,为插入排序
            for (int i=gap;i<nums.length;i++){ // 总共只需要循环len-gap此
                for (int j = i; j >=gap ; j-=gap) {  // 插入排序
                    if (nums[j]<nums[j-gap]){
                        swap(nums,j,j-gap);
                    }else {
                        break;
                    }
                }
            }
        }
        return nums;
    }
}

希尔排序是不稳定的,他的时间复杂度为O(nlog2n),空间复杂度为O(1)

三、选择排序

思想:每次选择数组元素中最小(最大)的元素放在序列的起始位置,直到全部待排序的数据元素排完

public class SelectSort implements SortMethod{
    @Override
    public int[] sort(int[] nums) {
        int len = nums.length;
        for (int i = 0; i < len; i++) {

            int location = i ;//记录最小值的位置
            for (int j = i; j < len; j++) {
                if (nums[location]>nums[j]){
                    location=j;
                }
            }
            if (i!=location){
                swap(nums,i,location);
            }
        }
        return nums;
    }
}

复杂度分析:O(n2),不稳定

四、堆排序

是一棵顺序存储完全二叉树

其中每个结点的关键字都不大于其孩子结点的关键字,这样的堆称为小根堆

其中每个结点的关键字都不小于其孩子结点的关键字,这样的堆称为大根堆

举例来说,对于n个元素的序列{R0, R1, ... , Rn}当且仅当满足下列关系之一时,称之为堆:

Ri <= R2i+1 且 Ri <= R2i+2 (小根堆)

Ri >= R2i+1 且 Ri >= R2i+2 (大根堆)

其中i=1,2,…,n/2向下取整;

如上图所示,序列R{3, 8, 15, 31, 25}是一个典型的小根堆。

堆中有两个结点,元素3和元素8。

元素3在数组中以R[0]表示,它的左孩子结点是R[1],右孩子结点是R[2]。

元素8在数组中以R[1]表示,它的左孩子结点是R[3],右孩子结点是R[4],它的父结点是R[0]。可以看出,它们满足以下规律

设当前元素在数组中以R[i] 表示,那么,

(1) 它的左孩子结点是:R[2*i+1] ;

(2) 它的右孩子结点是:R[2*i+2] ;

(3) 它的父结点是:R[(i-1)/2] ;

(4) R[i] <= R[2*i+1] 且 R[i] <= R[2i+2]。

首先,按堆的定义将数组R[0..n]调整为堆(这个过程称为创建初始堆),交换R[0]和R[n];

然后,将R[0..n-1]调整为堆,交换R[0]和R[n-1];

如此反复,直到交换了R[0]和R[1]为止。

以上思想可归纳为两个操作:

(1)根据初始数组去构造初始堆(构建一个完全二叉树,保证所有的父结点都比它的孩子结点数值大)。

(2)每次交换第一个和最后一个元素,输出最后一个元素(最大值),然后把剩下元素重新调整为大根堆。

当输出完最后一个元素后,这个数组已经是按照从小到大的顺序排列了。

先通过详细的实例图来看一下,如何构建初始堆。

设有一个无序序列 { 1, 3, 4, 5, 2, 6, 9, 7, 8, 0 }。

构造了初始堆后,我们来看一下完整的堆排序处理:

还是针对前面提到的无序序列 { 1, 3, 4, 5, 2, 6, 9, 7, 8, 0 } 来加以说明。

比如如下数组 {57, 40, 38, 11, 13, 34, 48, 75, 6, 19, 9, 7}堆排序前如下:

进行堆排序后如下:

最大堆的存储结构如下:

接着,最后一步,堆排序,进行(n-1)次循环。

相信,通过以上两幅图,应该能很直观的演示堆排序的操作处理。

看完上面所述的流程你至少有一个疑问:

如何确定最后一个非叶子结点?

其实这是有一个公式的,设二叉树结点总数为 n,则最后一个非叶子结点是第⌊n/2⌋个。

代码:

package com.example.demo.paixu;

import java.util.Arrays;

/**
 * @Author: liyangjing
 * @Date: 2022/03/21/11:08
 * @Description:
 */
public class HeapSort {

    public static void main(String[] args) {
        //堆排序原理:堆排序是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
        //大顶堆:arr[i]>=arr[i*2+1]&&arr[i]>=arr[i*2+2]
        //小顶堆:arr[i]<=arr[i*2+1]&&arr[i]<=arr[i*2+2]
        int[] arr = {4,21,5,86,74,12,57,42};
        //求出最大的非叶子节点的索引
        int startIndex=(arr.length-1)/2;
        //循环开始调,最大的非叶子节点多大,则循环几次,从最大的非叶子节点开始调
        for (int i = startIndex; i >=0; i--) {
            toMaxheap(arr,arr.length,i);
        }
        //以上的运行结果已经达到大顶堆的效果,此时只需将根节点的元素与最后一个叶子节点的元素互换就行,并递归将剩余元素继续转化成大顶堆
        for (int i = arr.length-1; i > 0; i--) {
            int t=arr[i];
            arr[i]=arr[0];
            arr[0]=t;
            //将剩余元素继续转化为大顶堆结构
            toMaxheap(arr,i,0);
        }
        System.out.println(Arrays.toString(arr));
    }

    /**
     *
     * @param arr   //要进行排序的数组
     * @param size  //要排序的范围
     * @param startIndex  //起始的索引位置
     */
    private static void toMaxheap(int[] arr, int size, int startIndex) {
        //求出左右节点的索引
        int leftNodeIndex=startIndex*2+1;
        int rightNodeIndex=startIndex*2+2;
        //假设刚开始最大数的索引就是起始索引
        int maxIndex=startIndex;
        //求出最大节点所对应的索引
        if(leftNodeIndex<size&&arr[leftNodeIndex]>arr[maxIndex]){
            maxIndex=leftNodeIndex;
        }
        if(rightNodeIndex<size&&arr[rightNodeIndex]>arr[maxIndex]){
            maxIndex=rightNodeIndex;
        }
        //调换位置,将最大节点放在大顶堆的根节点处
        if(maxIndex!=startIndex){
            int t=arr[startIndex];
            arr[startIndex]=arr[maxIndex];
            arr[maxIndex]=t;
            //互换完之后可能会影响最大节点以下的大顶堆结构,所以这里需要递归调用方法,保证每个子树都是大顶堆结构
            toMaxheap(arr,size,maxIndex);
        }
    }
}

五、冒泡排序

非常经典的排序算法。

思想:将两个元素两两进行排序,遍历完一次都会把最大(小)的元素放在了后面,是一种非常容易理解的排序方法。

public class BubbleSort implements SortMethod{
    @Override
    public int[] sort(int[] nums) {
        int len = nums.length;
        for (int i = 1; i < len; i++) {
            for (int j = 0; j < len-i; j++) {
                if (nums[j] > nums[j+1]){
                    swap(nums,j,j+1);
                }
            }
        }
        return nums;
    }
}

复杂度分析:时间复杂度O(n2), 稳定

六、快速排序

快速排序属于交换排序的一种

1.单边循环

单边循环主要步骤:

  1. 找到基准点
  2. 循环遍历,发现比基准点小的则交换
  3. 分而治之,分治算法 递归,打印
public class QuickSort implements SortMethod{
    @Override
    public int[] sort(int[] nums) {
        s(nums,0,nums.length-1);
        return nums;
    }

    public void s(int[] numbers , int left ,int right){
        if (left>=right){
            return;
        }
        // 1.找到基准点
        int pv = numbers[right];
        // 2. 低点
        int i  = left;
        // 3.循环遍历
        for (int j = left; j <=right ; j++) {
            if (numbers[j]<pv){
                swap(numbers,j,i);
                i++;
            }
        }
        //中间元素 右边都比他大 ,左边都比他小
        swap(numbers,i,right);
        System.out.println(Arrays.toString(numbers));
        s(numbers,left,i-1);
        s(numbers,i+1,right);
    }
}

2.双边循环

双边循环主要步骤:

  1. 找到基准点 左边
  2. 双指针比较 ,分别找到不符合要求的 进行交换。(先找到小的,在找到大的)
  3. 交换基准点值
  4. 分而治之
//双边循环
    public void s1(int[] numbers ,int left ,int right){
        if (left>=right){
            return;
        }
        // 1.找到基准点
        int pv = numbers[left];
        // 保证左右点
        int i = left;
        int j = right;
        while (i<j){
            while (i<j && pv<numbers[j]){
                j--;
            }
            while (i<j && pv >= numbers[i]){
                i++;
            }
            swap(numbers,i,j);
        }
        swap(numbers,left,j);
        System.out.println(Arrays.toString(numbers));
        s1(numbers,left,j-1);
        s1(numbers,j+1,right);
    }

时间复杂度: O(nlogn) 最坏情况O(n2) 他是不稳定的排序算法

七、归并排序

归并算法采用非常经典的分治策略,每次把序列分成n/2的长度,将问题分解成小问题,由复杂变简单。

复杂度:时间复杂度O(NlogN),空间复杂度O(N)。

稳定性:稳定

    /**
     * 归并排序
     * 简介:将两个(或两个以上)有序表合并成一个新的有序表 即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列
     * 时间复杂度为O(nlogn)
     * 稳定排序方式
     * @param nums 待排序数组
     * @return 输出有序数组
     */
    public static int[] sort(int[] nums, int low, int high) {
        int mid = (low + high) / 2;
        if (low < high) {
            // 左边
            sort(nums, low, mid);
            // 右边
            sort(nums, mid + 1, high);
            // 左右归并
            merge(nums, low, mid, high);
        }
        return nums;
    }

    /**
     * 将数组中low到high位置的数进行排序
     * @param nums 待排序数组
     * @param low 待排的开始位置
     * @param mid 待排中间位置
     * @param high 待排结束位置
     */
    public static void merge(int[] nums, int low, int mid, int high) {
        int[] temp = new int[high - low + 1];
        int i = low;// 左指针
        int j = mid + 1;// 右指针
        int k = 0;

        // 把较小的数先移到新数组中
        while (i <= mid && j <= high) {
            if (nums[i] < nums[j]) {
                temp[k++] = nums[i++];
            } else {
                temp[k++] = nums[j++];
            }
        }

        // 把左边剩余的数移入数组
        while (i <= mid) {
            temp[k++] = nums[i++];
        }

        // 把右边边剩余的数移入数组
        while (j <= high) {
            temp[k++] = nums[j++];
        }

        // 把新数组中的数覆盖nums数组
        for (int k2 = 0; k2 < temp.length; k2++) {
            nums[k2 + low] = temp[k2];
        }
    }

复杂度:时间复杂度O(NlogN),空间复杂度O(N)。

稳定性:稳定