7大比较排序+3个非比较排序

排序

前言

稳定性

一组待排序的数据中,如果存在相同的数值,排序后多个相同数值的相对位置不变,就是稳定的;否则不稳定

image-20230303185146242

排序分类

内部排序:数据都在内存中进行操作的,大多数排序都是这种

外部排序:数据很大,内存装不下,这时候就需要运用外部排序了

image-20230303185535472

7大比较排序

插入排序

类似于:我们平时玩的扑克牌,在我们拿牌的时候,都是将小的往前面进行插入(这就是排升序)

image-20230303190018431

动图展示

单趟:有一个有序数组,现在新来一个值,需要将这个值放入该数组,并使这个数组仍然有序。这时我们就可以将这个元素与数组中的元素从后往前进行比较,找到自己合适的位置,然后进行插入

单趟

复合:在自己不知道有几个有序的时候,这时候我们就可以把第一个数当作有序的,然后从第二个数开始,依次进行单趟排序

插入排序

代码
public static void insertSort(int[] array) {
        for (int i = 1; i < array.length; i++) {
            int end = i-1;  // 当while循环结束的时候,end+1就是要待排序元素的位置了
            int tmp = array[i];   // 待排序元素
            while (end >= 0) {
                if(array[end] > tmp) {
                    array[end+1] = array[end];
                    end--;
                } else {
                    break;
                }
            }

            // 两种情况
            // 1. end = -1
            // 2. array[end] <= tmp
            array[end + 1] = tmp;
        }
    }
总结
  1. 插入排序是一个稳定排序
  2. 时间复杂度:
    • 最好:本来数组处于顺序情况(O(N))
    • 最坏:每个元素都需要挪动 i 次(O(n*(1+2+3+……+ n-1))----> O(N^2))
  3. 空间复杂度:
    • O(1)
  4. 使用场景:数据量少且趋近于有序的情况下,直接插入排序的时间复杂度趋近于 O(N)

希尔排序

针对插入排序的升级,运用分组的思想(先每一组都趋近于有序,然后慢慢变成整体趋近于有序了

先选定一个整数gap,把待排序文件中所有记录分成gap个组,所有距离为gap的记录分在同一组内,并对每一组内的元素进行插入排序。然后将gap逐渐减小重复上述分组和排序的工作。当gap=1时,就直接对整个数组进行插入排序了**【这时排序的数组已经趋近于有序了,就很适合用插入排序了】**。

image-20230303192751780

画图展示

89ce59c64f8649f58dd77b27903cfa79

动图

007b76889ad8412cabc552d8ab2c5226

代码
public static void shellSort(int[] array) {
        int gap = array.length;
        while (gap > 1) {
            gap /= 2;
            shellSortHelper(array,gap);
        }
        shellSortHelper(array,1);
    }

    private static void shellSortHelper(int[] array, int gap) {
        for (int i = gap; i < array.length; i++) {
            int end = i-gap;
            int tmp = array[i];   // 待排序元素
            while (end >= 0) {
                if(array[end] > tmp) {
                    array[end+gap] = array[end];
                    end-=gap;
                } else {
                    break;
                }
            }

            array[end + gap] = tmp;
        }
    }
总结
  1. 希尔排序是一个不稳定排序
  2. 时间复杂度:希尔排序时间复杂度不要计算,预估:O(N^1.3 ~ N^1.5)
  3. 空间复杂度:O(1)
  4. 当 gap > 1 时都是预排序,gap = 1 的时候,才是真正的排序

选择排序

每趟找一个最小的值放待排序序列的最前面

升级版:一趟遍历在待排序区间 找一个最小的 + 一个最大的

动图展示

选择排序

代码
public static void selectSort(int[] array) {
        for (int i = 0; i < array.length; i++) {
            int tmp = i;
            for (int j = i; j < array.length; j++) {
                if(array[j] < array[tmp]) {
                    tmp = j;
                }
            }

            // 交换 tmp,i的值
            swap(array,tmp,i);
        }
    }
改进

注意:当交换最小值后,在交换最大值(这就需要考虑先交换的最小值时会不会把你需要的最大值给换走了)

public static void selectSort1(int[] array) {
        int left = 0, right = array.length-1;
        while (left < right) {
            int minIndex = left;
            int maxIndex = right;

            for (int i = left; 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);
            right--;
            left++;
        }
    }
总结
  1. 选择排序是不稳定的
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1)
  4. 使用改进版本,可以提高一半时间复杂度

堆排序

堆介绍

堆是一颗完全二叉树,存储方式是拿数组存储的【不存在浪费空间】,根据完全二叉树的层序遍历来标记下标

小根堆:根节点的值 < 孩子节点的值

大根堆:根节点的值 > 孩子节点的值

二叉树的结论:

  • 已知父亲节点下标 i ,左孩子下标 2*i+1 右孩子下标 2 * i+2
  • 已知孩子下标 i ,父亲节点下标 (i - 1)/ 2

image-20230304163513597

建堆调整过程【自顶向下】

都是从最后一棵子树开始调整,向下调整

最后一棵子树的父节点下标:p = (array.length-1-1) / 2,(最后一个元素的下标 - 1)/ 2;调整完后 p-- ,直到调整完 0 下标的这棵树,每次调整的结束条件:1. 此树符合条件了;2. 访问的下标 < array.length

image-20230304164934076

向下调整
public void creatHeap(int[] array) {
    for (int p = (array.length-1-1)/2; p >= 0; p--) {
        shiftDown(array,p,array.length);
    }
}

private void shiftDown(int[] array, int root, int len) {
    int parent = root;
    int child = 2*parent+1;  // 左孩子
    while (child < len) {
        // 至少有一个孩子,大根堆:找到两个孩子中的较大值,再与 parent比较 > parent就换,否则不交换
        if(child + 1 < len && array[child] < array[child+1]) {
            child = child+1;
        }

        // 判断array[child] 与 array[parent]的大小
        if(array[child] > array[parent]) {
            int tmp = array[child];
            array[child] = array[parent];
            array[parent] = tmp;
            parent = child;
            child = 2*parent+1;
        } else {
            // 此树满足条件,不需要调整了
            break;
        }
    }
}
自底向上建堆【shiftDown】的复杂度:O(N)

image-20230304171743455

向上调整
private void shiftUp(int child) {
        int parent = (child-1)/2;

        while (child > 0) {
            if(elem[parent] < elem[child]) {
                int tmp = elem[child];
                elem[child] = elem[parent];
                elem[parent] = tmp;
                child = parent;
                parent = (child-1)/2;
            } else {
                break;
            }
        }
    }
向上调整建堆【每次push,然后调用shiftUp】的时间复杂度

因为每次都要向下调整,而有 n 个数需要调整,所以复杂度为:N * logN(需要调整个数 * 树的高度)

堆排序

排序前需要建堆

从小到大排序:建大堆【每次将堆顶元素放最后,然后调整 0 这棵树,就相当于最大的数排好序了】

从大到小排序:建小堆【每次将堆顶元素放最后,然后调整 0 这棵树,就相当于最小的数排好序了】

public void HeapSort() {
        // 从小到大----建立大堆
        // 每次取堆顶放到最后【最大值放好位置了】,然后调整0~end-1
        int end = usedSize-1;
        while (end > 0) {
            int tmp = elem[0];
            elem[0] = elem[end];
            elem[end] = tmp;
            end--;
            shiftDown(0,end);
        }
    }
总结
  1. 时间复杂度:N * logN 个数 * 树的高度
  2. 空间复杂度:
  3. 稳定性:不稳定

冒泡排序

外层循环:需要排的趟数(数组长度-1),每一趟下来,就排好一个数

内层循环:每一趟排序需要比较的对数(两两比较的对数)

动图展示

冒泡排序

代码
public static void bubbleSort(int[] array) {
        for (int i = 0; i < array.length-1; i++) {
            boolean flag = true;
            for (int j = 0; j < array.length-i-1; j++) {
                if(array[j] > array[j+1]) {
                    swap(array,j,j+1);
                    flag = false;
                }
            }
            if(flag) {
                break;
            }
        }
    }
总结
  1. 冒泡排序是稳定的
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1)

快速排序

基本思想:每次遍历,都找一个 pivot 基准值,这个基准值的左边都比它小,右边都比它大,然后递归右边找基准,递归左边找基准**【相当于每次遍历,排好一个数】**,【类似于二叉树,我们找根节点,然后分成左子树 和 右子树】

Hoare版本

将当前序列的最左边的数作为基准,然后右边找一个小于基准的,左边找一个大于基准的【注意左边做key,右边先找值;右边做key时,左线先找值】,然后两个交换;继续遍历找下一组,直到 left < right 就结束了,结束后,将 left下标 与 基准元素的下标交换

画图展示

image-20230304092934909

代码
private static void quick(int[] array, int low, int high) {
    // = 表示:当前序列只有一个元素了,就不需要递归了;> 表示:类似于左子树递归完了,递归右子树去了[右子树可能为 null]
        if(low >= high) return;

        int pivot = partition(array,low,high);
        quick(array,low,pivot-1);
        quick(array,pivot+1,high);
    }

    private static int partition(int[] array, int left, int right) {
        int key = array[left];
        int tmp = left;
        while (left < right) {
            // 注意这两个的while循环中的 array[] >= key 中的等于号,
            // 若没有等于号的情况下,array[left] == array[right]:内部两个while循环都进不去,外层死循环了
            while (left < right && array[right] >= key) {
                right--;
            }

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

            swap(array,left,right);
        }
        swap(array,left,tmp);

        return left;
    }

挖坑法

首先记录 key = array[left] 相当于把 当前序列的 left 作为坑,右边找一个大于 key 的放 left 坑里,右边的就成坑了;然后左边找一个小于 key 的,放 right 坑里;以此类推,然后 left < right时,将 key 放left这个坑里

画图展示

image-20230304094925250

代码
pJavrivate static int Hole(int[] array, int left, int right) {
        int key = array[left];
        while (left < right) {
            // 注意这两个的while循环中的 array[] >= key 中的等于号,
            // 若没有等于号的情况下,array[left] == array[right]:内部两个while循环都进不去,外层死循环了
            while (left < right && array[right] >= key) {
                right--;
            }

            array[left] = array[right];

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

            array[right] = array[left];

        }
        array[left] = key;

        return left;
    }
前后指针法【了解】

思路:i 负责找一个小于 tmp 的值,d 负责保存 大于 tmp 的下标,最后 i d 交换

public static int partition1(int[] array, int left, int right) {
        int d = left + 1;
        int tmp = array[left];
        for(int i = left + 1; i<=right; i++) {
            if(array[i] < tmp) {
                swap(array,i,d);
                d++;
            }
        }
        swap(array,d-1,left);
        return d-1;
    }
三数取中法【优化】

left,right,mid 中取第二大的值的下标作为最终的哨兵key,然后用partition函数找 pivot,这就使每次划分序列都较为均匀,有效的减少了递归深度

当数组趋近于有序的时候,可以采用插入排序进行优化

 public static void insertSortRange(int[] array, int left, int right) {
        for (int i = left; i <= right; i++) {
            int end = i - 1;  // 当while循环结束的时候,end+1就是要待排序元素的位置了
            int tmp = array[i];   // 待排序元素
            while (end >= left) {
                if (array[end] > tmp) {
                    array[end + 1] = array[end];
                    end--;
                } else {
                    break;
                }
            }

            array[end + 1] = tmp;
        }
    }

private static int medianOfThreeIndex(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 {
            if(array[mid] < array[right]) {
                return right;
            } else if(array[mid] > array[left]) {
                return left;
            } else {
                return mid;
            }
        }
    }

    private static void quick(int[] array, int low, int high) {
        if(low >= high) return;
        if(high - low < 40) {
            insertSortRange(array,low,high);
        }

        // 减少递归深度
        int index = medianOfThreeIndex(array,low,high);

        swap(array,low,index);

        int pivot = partition1(array,low,high);

        quick(array,low,pivot-1);
        quick(array,pivot+1,high);
    }
非递归版本

借助栈,还是建立在找基准的基础上,找到 pivot 时,会划分成两个待排序序列,然后将两个序列的区间下标放入栈中,每次从栈中弹出两个值,然后针对这个区间找基准,最后栈为空,就排好序了

public static void norQuick(int[] array) {
        Stack<Integer> stack = new Stack<>();
        int left = 0, right = array.length-1;
        int pivot = Hole(array,left,right);
        // 表示[left,pivot]至少有两个元素
        if(pivot > left+1) {
            stack.push(left);
            stack.push(pivot-1);
        }
        if(pivot < right-1) {
            stack.push(pivot+1);
            stack.push(right);
        }
        while (!stack.isEmpty()) {
            right = stack.pop();
            left = stack.pop();

            pivot = Hole(array,left,right);
            // 表示[left,pivot]至少有两个元素
            if(pivot > left+1) {
                stack.push(left);
                stack.push(pivot-1);
            }
            if(pivot < right-1) {
                stack.push(pivot+1);
                stack.push(right);
            }
        }
    }
总结
  1. 时间复杂度:
    • 最好:每次 pivot 都是在当前序列的最中间,将当前序列完美分成对此的两部分,每层 n 个数,完全二叉树高度:logN,最好就是 N*logN
    • 最坏:单分支树 O(N^2)
  2. 空间复杂度:
    • 最好:完全二叉树的高度:O(logN)
    • 最坏:单分支树的高度(数组的长度) O(N)
  3. 稳定性:不稳定

还可以看看这篇文章:快排优化

归并排序

也是采用分治的思想,先将数组拆分成一个一个的元素,然后两个两个合并,两个合并结束;四个四个合并……一直到完全排好序(若是将两个有序集合合并,称为二路归并

图解

image-20230304215559500

归并排序

递归版本
public static void mergeSort(int[] array) {
        mergeSortIntenal(array,0,array.length-1);
    }

    private static void mergeSortIntenal(int[] array, int low, int high) {
        if(low >= high) {
            return;
        }

        int mid = (low+high)/2;
        mergeSortIntenal(array,low,mid);
        mergeSortIntenal(array,mid+1,high);

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

    private static void merge(int[] array, int low, int mid, int high) {
        // 1. 合并[low,mid],[mid+1,high]  创建数组:high-low+1
        int s1 = low;
        int e1 = mid;
        int s2 = mid+1;
        int e2 = high;

        int[] tmpArr = new int[high-low+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++];
        }

        // 将tmpArr中的数据放入array
        System.arraycopy(tmpArr,0,array,low,k);
    }
非递归版本
public static void mergeSort1(int[] array) {
        int gap = 1;
        while (gap < array.length) {
            // for 循环控制2个2个排序后合并;4个4个排序后合并……
            for (int i = 0; i < array.length; i+=2*gap) {
                int left = i;
                int mid = left + gap - 1;
                if(mid >= array.length) {
                    mid = array.length-1;
                }
                int right = mid + gap;
                if(right >= array.length) {
                    right = array.length-1;
                }

                merge(array,left,mid,right);
            }
            gap *= 2;
        }
    }
总结
  1. 时间复杂度:N * log(N)
  2. 空间复杂度:O(N)
  3. 稳定性:稳定

3大非比较排序

计数排序

需要创建一个数组,数组大小(最大值 - 最小值 + 1), 然后将数组中的值的个数放入对应的下标中

public static void func(int[] array) {
        // 1. 找最大最小值
        int max = 0, min = Integer.MAX_VALUE;
        for (int i = 0; i < array.length; i++) {
            if(array[i] > max) {
                max = array[i];
            }

            if(array[i] > min) {
                min = array[i];
            }
        }

        // 2. 创建数组
        int[] arr = new int[max-max+1];
        Arrays.fill(arr,min-1);  // 填充一个数组中没有的元素
        for (int i = 0; i < array.length; i++) {
            arr[array[i]-min]++;
        }

        // 将元素填充进array
        int k = 0;
        for (int i = 0; i < arr.length; i++) {
            if(arr[i] > 0) {
                for (int j = 0; j < arr[i]; j++) {
                    array[k++] = arr[i]+min;
                }
            }
        }
    }

基数排序

根据每位数进行排序的(个位,十位,百位……),排到最高位的时候就是有序了,所以需要 存放每位上的数字的10个队列(存放 0 - 9)

image-20230304112221635

桶排序

这个也需要队列:感觉就是 基数排序 + 计数排序

桶的数量:最大值 - 最小值 + 1,然后桶里面放的也是每个元素的个数,最后遍历拿值就可以了

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值