十大排序算法

image.png

 

冒泡排序(Bubble Sort)

 

  • 执行流程

① 从头开始比较每一对相邻元素,如果第1个比第2个大,就交换它们的位置;✓ 执行完一轮后,最末尾那个元素就是最大的元素

② 忽略 ① 中曾经找到的最大元素,重复执行步骤 ①,直到全部元素有序。

    private static void bubbleSort(int[] arr) {
        // 边界条件判断(注:细节问题,安全编码)
        if (arr==null || arr.length<2) return;

        for (int end = arr.length - 1; end > 0; end--) {
            for (int begin = 1; begin <= end ; begin++) {
                if (arr[begin] < arr[begin - 1]){
                    int tmp = arr[begin];
                    arr[begin] = arr[begin - 1];
                    arr[begin - 1] = tmp;
                }
            }
        }
        ArrayUtil.print(arr);

    }

另一个版本理解思路

    //N个数字冒泡排序,总共要进行N-1趟比较,每趟的排序次数为(N-i)次比较
    private static void bubbleSort(int[] arr) {
        // 边界条件判断(注:细节问题,安全编码)
        if (arr==null || arr.length<2) return;

        // N个数字要排序完成,总共进行N-1趟排序,每i趟的排序次数为(N-i)次
        // 外层 i 控制循环多少趟,内层 j 控制每一趟的循环次数
        for (int i = 1; i < arr.length; i++){
            for (int j = 0; j < arr.length - i; j++) {
                if (arr[j] > arr[j + 1]) {
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
        }
        ArrayUtil.print(arr);
    }
  • 优化-1- 如果序列已经完全有序,可以提前终止冒泡
private static void bubbleSortOptimization1(int[] arr) {
        // 边界条件判断(注:细节问题,安全编码)
        if (arr==null || arr.length<2) return;

        // **优化点**  引入标记值flag 如果在某一次内循环排序比较时,未发生位置交换,说明排序已经完成,无序排序,则提前break 终止循环
        for (int end = arr.length - 1; end > 0; end--) {
            boolean flag = true;
            for (int begin = 1; begin <= end ; begin++) {
                if (arr[begin] < arr[begin - 1]){
                    int tmp = arr[begin];
                    arr[begin] = arr[begin - 1];
                    arr[begin - 1] = tmp;
                    flag = false;
                }
            }
            if (flag) break;
        }
        ArrayUtil.print(arr);
}
 
  • 优化-2- 如果序列尾部已经局部有序,可以记录最后1次交换的位置,减少比较次
 private static void bubbleSortOptimization2(int[] arr) {
        //边界条件判断(注:细节问题,安全编码)
        if (arr == null || arr.length < 2) return;

        // **优化点** 引入位置标记值swagIndex,记录某次循环最后1次交换的位置,如果序列尾部已经局部有序,减少比较次数
        for (int end = arr.length - 1; end > 0; end--) {
            int swagIndex = 1;//此处数值的确定只要小于等于1即可
            for (int begin = 1; begin <= end ; begin++) {
                if (arr[begin] < arr[begin - 1]){
                    int tmp = arr[begin];
                    arr[begin] = arr[begin - 1];
                    arr[begin - 1] = tmp;
                    swagIndex = begin;
                }
            }
            end = swagIndex;
        }
        ArrayUtil.print(arr);
}

完整代码 >>> 冒泡排序

 

选择排序(Selection Sort)

◼ 执行流程

① 从序列中找出最大的那个元素,然后与最末尾的元素交换位置

✓ 执行完一轮后,最末尾的那个元素就是最大的元素

② 忽略 ① 中曾经找到的最大元素,重复执行步骤 ①

 private static void baseSelectionSort(int[] arr) {
        for (int end = arr.length - 1; end > 0 ; end--) {
            int maxIndex = 0;
            for (int begin = 1; begin <= end; begin++) {
                if (arr[begin] > arr[maxIndex]){
                    maxIndex = begin;
                }
            }
            ArrayUtil.swap(arr,maxIndex,end);
        }
        ArrayUtil.print(arr);
    }

 

◼ 选择排序的交换次数要远远少于冒泡排序,平均性能优于冒泡排序

◼ 最好、最坏、平均时间复杂度:O(n2),空间复杂度:O(1),属于不稳定排序

 

堆排序(Heap Sort)

堆排序可以认为是对选择排序的一种优化

◼ 执行流程

① 对序列进行原地建堆(heapify)

② 重复执行以下操作,直到堆的元素数量为 1

✓ 交换堆顶元素与尾元素

✓ 堆的元素数量减 1

✓ 对 0 位置进行 1 次 siftDown 操作

image

private static void baseHeapSort(int[] arr) {
        // 原地建堆
        heapSize = arr.length;
        for (int i = (heapSize >> 1) - 1; i >= 0; i--) {
            siftDown(arr,i);
        }

        while (heapSize > 1) {
            // 交换堆顶元素和尾部元素
            ArrayUtil.swap(arr,0, --heapSize);

            // 对0位置进行siftDown(恢复堆的性质)
            siftDown(arr,0);
        }
        ArrayUtil.print(arr);
    }

    private static void siftDown(int[] array, int index) {
        int element = array[index];

        int half = heapSize >> 1;
        while (index < half) { // index必须是非叶子节点
            // 默认是左边跟父节点比
            int childIndex = (index << 1) + 1;
            int child = array[childIndex];

            int rightIndex = childIndex + 1;
            // 右子节点比左子节点大
            if (rightIndex < heapSize &&
                    ArrayUtil.cmp(array[rightIndex], child) > 0) {
                child = array[childIndex = rightIndex];
            }

            // 大于等于子节点
            if (ArrayUtil.cmp(element, child) >= 0) break;

            array[index] = child;
            index = childIndex;
        }
        array[index] = element;
    }

◼ 最好、最坏、平均时间复杂度:O(nlogn),空间复杂度:O(1),属于不稳定排序

 

插入排序(Insertion Sort)

image

◼ 执行流程

① 在执行过程中,插入排序会将序列分为2部分

✓ 头部是已经排好序的,尾部是待排序的

② 从头开始扫描每一个元素

✓ 每当扫描到一个元素,就将它插入到头部合适的位置,使得头部数据依然保持有序

   private static void baseInsertionSort(int[] arr) {
        for (int begin = 1; begin < arr.length; begin++) {
            int currentIndex = begin;
            while (currentIndex > 0 && arr[currentIndex] < arr[currentIndex - 1]){
                ArrayUtil.swap(arr,currentIndex,--currentIndex);
            }
        }
        ArrayUtil.print(arr);
    }

逆序对(Inversion)

  • 数组 <2,3,8,6,1> 的逆序对为:<2,1> <3,1> <8,1> <8,6> <6,1>,共5个逆序对

image.png

◼ 插入排序的时间复杂度与逆序对的数量成正比关系

逆序对的数量越多,插入排序的时间复杂度越高

◼ 最坏、平均时间复杂度:O(n2)

◼ 最好时间复杂度:O(n)

◼ 空间复杂度:O(1)

◼ 属于稳定排序

◼ 当逆序对的数量极少时,插入排序的效率特别高

甚至速度比 O nlogn 级别的快速排序还要快

◼ 数据量不是特别大的时候,插入排序的效率也是非常好的

 

  • 优化-1- 将【交换】转为【挪动】

◼ 思路

① 先将待插入的元素备份

② 头部有序数据中比待插入元素大的,都朝尾部方向挪动1个位置

③ 将待插入元素放到最终的合适位置

 

image.png

   private static void optimizationInsertionSort(int[] arr) {
        for (int begin = 1; begin < arr.length; begin++) {
            int currentIndex = begin;
            int currentValue = arr[begin];
            while (currentIndex > 0 && arr[currentIndex] < arr[currentIndex - 1]) {
                arr[currentIndex] = arr[currentIndex - 1];
                currentIndex--;
            }
            arr[currentIndex] = currentValue;

        }
    }

归并排序(Merge Sort)

归并排序是采用分治法的一个非常典型的应用。

归并排序的思想就是先递归分解数组,再合并数组。

思路

先考虑合并俩个有序数组,基本思路是比较俩个数组的最前面的数,谁小就先取谁,取了后相应的指针就往后移一位。然后再比较,直到一个数组为空,最后把另一个数组的剩余部分复制过来即可。

再考虑递归分解,基本思路是将数组分解成left和right,如果这俩个数组内部数据是有序的,那么就可以用上面合并数组的方法将这俩个数组合并排序。如何让这俩个数组内部是有序的?可以再鹅肉粉,知道分解出的小组只含有一个元素时为止,此时认为该小组内部已有序。然后合并排序相邻二个小组即可。

  • 执行流程

① 不断地将当前序列平均分割成2个子序列

✓ 直到不能再分割(序列中只剩1个元素)

② 不断地将2个子序列合并成一个有序序列

✓ 直到最终只剩下1个有序序列

  1.      * 将待排序序列R[0...n-1]看成是n个长度为1的有序序列,将相邻的有序表成对归并,得到n/2个长度为2的有序表;将这些有序序列再次归并, 
  2.      * 得到n/4个长度为4的有序序列;如此反复进行下去,最后得到一个长度为n的有序序列。 
  3.      * 综上可知:归并排序其实要做两件事:(1)“分解”——将序列每次折半划分。(2)“合并”——将划分后的序列段两两合并后排序。 

image.png

image

public class Merge_Sort extends Sort{
    private static int[] leftArray;
    private static int[] array = arr;

    public static void main(String[] args) {
        //准备一个新的数组,用来备份需要交换的子数组序列
        leftArray = new int[array.length >> 1];
        ArrayUtil.print(leftArray);
        mergeSort(array);
        ArrayUtil.print(array);
    }

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

    private static void sort(int begin, int end) {
        //排序合法性校验(必须,否则会在递归过程中出现 StackOverflowError)
        if ((end - begin) < 2 ) return;

        int mid = (begin + end) >> 1;
        sort(begin, mid);
        sort(mid, end);
        merge(begin,mid,end);
    }

    private static void merge(int begin, int mid, int end) {
        //左边数组子序列
        int li = 0, le = mid - begin;
        //右边数组子序列
        int ri = mid, re = end;
        //填充指针
        int ai = begin;

        // 备份左边数组
        for (int i = li; i < le; i++) {
            leftArray[i] = array[begin + i];
        }

        // 如果左边还没有结束
        while (li < le) {
            if (ri < re && (array[ri] -  leftArray[li]) < 0) {
                array[ai++] = array[ri++];
            } else {
                array[ai++] = leftArray[li++];
            }
        }
    }

}

 

 

快速排序(Quick Sort)

快速排序通常明显比同为Ο(n log n)的其他算法更快,而且快排采用了分治法的思想,因此常被采用。

image

① 从序列中选择一个轴点元素(pivot)

✓ 假设每次选择 0 位置的元素为轴点元素

② 利用 pivot 将序列分割成 2 个子序列

✓ 将小于 pivot 的元素放在pivot前面(左侧)

✓ 将大于 pivot 的元素放在pivot后面(右侧)

✓ 等于pivot的元素放哪边都可以

③ 对子序列进行 ① ② 操作

✓ 直到不能再分割(子序列中只剩下1个元素)

  1. 从数列中挑出一个元素作为基准数。
  2. 分区过程,将比基准数大的放到右边,小于或等于它的数都放到左边。
  3. 再对左右区间递归执行第二步,直至各区间只有一个数。
public class Quick_Sort extends Sort {
    public static void main(String[] args) {
        ArrayUtil.print(arr);
        sort(0,arr.length);
        ArrayUtil.print(arr);
    }
    private static void sort(int begin, int end){
        //排序数组合法性校验
        if ((end - begin) < 2) return ;

        int mid = pivotIndex(begin, end);
        sort(begin, mid);
        sort(mid + 1, end);
    }
    private static int pivotIndex(int begin, int end) {
        //选取索引为0即数组的第一个元素作为轴点 pivot
        //备份轴点元素
        int pivot = arr[begin];
        // end指向最后一个元素
        end--;
        while(begin < end){
            //将小于 pivot 的元素放在pivot前面(左侧)
            while(begin < end){
                if (pivot < arr[end]){
                    end--;
                }else{
                    arr[begin++] = arr[end];
                    break;
                }
            }
            //将大于 pivot 的元素放在pivot后面(右侧)
            while(begin < end){
                if (arr[begin] < pivot){
                    begin++;
                }else{
                    arr[end--] = arr[begin];
                    break;
                }
            }
        }
        //将pivot放在基准位置
        arr[begin] = pivot;
        return begin;
    }
}

 

希尔排序(Shell Sort)

希尔排序靶序列看做是一个矩阵,分成m列,逐列进行排序

  • m从某个证书逐渐减为1
  • 当m为1是,整个序列将完全有序

因此,希尔排序,也称递减增量排序算法,实质是插入排序的优化,即分组插入排序。属于非稳定排序算法。

矩阵的列数取决于步长序列(step sequence),比如步长序列为{1,2,4,8,16,32……},就代表依次分成32列、16列、8列、4列、1列进行排序。

希尔本人给出的步长序列是 𝑛/2𝑘,比如 𝑛 为16时,步长序列是{1, 2, 4, 8}

算法思想

将数组列在一个表中并对列分别进行插入排序,重复这个过程,不过每次用更长的列(步长大,列数少)来进行。最后整个表就只有一列了。将数组转换至表示为了更好地理解这个算法。算法本身还是使用数组进行排序。

public class Shell_Sort extends Sort {
    public static void main(String[] args) {
        shellSort(arr);
        ArrayUtil.print(arr);
    }
    private static void shellSort(int[] arr) {
        //定义步长参数
        int step;
        int temp,i,j;
        for(step = arr.length/2;step > 0;step /= 2){ //增量为len/2 len/4 len/8.....1
            for(i=step;i<arr.length;i++){ //对每个子序列进行插入排序
                temp = arr[i];
                for(j=i;j>=step;j -= step){
                    if(temp < arr[j-step]){
                        arr[j] = arr[j-step];
                    }
                    else
                        break;
                }
                arr[j] = temp;
            }
        }
    }
}


目前已知的最好的步长序列,最坏情况时间复杂度是 O(n4/3) ,1986年由Robert Sedgewick提出

image.png

private List<Integer> sedgewickStepSequence() {
        List<Integer> stepSequence = new LinkedList<>();
        int k = 0, step = 0;
        while (true) {
            if (k % 2 == 0) {
                int pow = (int) Math.pow(2, k >> 1);
                step = 1 + 9 * (pow * pow - pow);
            } else {
                int pow1 = (int) Math.pow(2, (k - 1) >> 1);
                int pow2 = (int) Math.pow(2, (k + 1) >> 1);
                step = 1 + 8 * pow1 * pow2 - 6 * pow2;
            }
            if (step >= array.length) break;
            stepSequence.add(0, step);
            k++;
        }
        return stepSequence;
    }

——————————————

计数排序(Counting Sort)

算法思想

统计每个整数在序列中出现的次数,进而推导出每个整数在有序序列中的索引

image.png

public class Counting_Sort extends Sort {

    public static void main(String[] args) {
        //基础版
        baseCountingSort();
        ArrayUtil.print(arr);
    }

    /**
     * 基础版本
     */
    private static void baseCountingSort() {

        //找出数组中最大的元素
        int max = 0;
        for (int i = 0; i < arr.length; i++) {
            if (arr[i] > max) max = arr[i];
        }

        //开辟内存空间,存储每个整数出现的次数
        int[] counts = new int[max + 1];
        //统计每个整数出现的次数
        for (int i = 0; i < arr.length; i++) {
            counts[arr[i]]++;
        }

        //根据整数出现的次数,对原数组进行排序填充
        int index = 0;
        for (int i = 0; i < counts.length; i++) {
            while(counts[i]-- > 0){
                arr[index++] = i;
            }
        }
    }
}

简单实现的这种方案,存在无法对负整数进行排序、及其浪费内存空间、是个不稳定的排序。

 

优化

改进思路

◼ 假设array中的最小值是 min

◼ array中的元素 k 对应的 counts 索引是 k – min

◼ array中的元素 k 在有序序列中的索引 counts[k – min] – p p 代表着是倒数第几个 k

◼ 比如元素 8 在有序序列中的索引 counts[8 – 3] – 1,结果为 7

◼ 倒数第 1 个元素 7 在有序序列中的索引 counts[7 – 3] – 1,结果为 6

◼ 倒数第 2 个元素 7 在有序序列中的索引 counts[7 – 3] – 2,结果为 5

// 找出最值
        int max = array[0];
        int min = array[0];
        for (int i = 1; i < array.length; i++) {
            if (array[i] > max) {
                max = array[i];
            }
            if (array[i] < min) {
                min = array[i];
            }
        }
        
        // 开辟内存空间,存储次数
        int[] counts = new int[max - min + 1];
        // 统计每个整数出现的次数
        for (int i = 0; i < array.length; i++) {
            counts[array[i] - min]++;
        }
        // 累加次数
        for (int i = 1; i < counts.length; i++) {
            counts[i] += counts[i - 1];
        }
        
        // 从后往前遍历元素,将它放到有序数组中的合适位置
        int[] newArray = new int[array.length];
        for (int i = array.length - 1; i >= 0; i--) {
            newArray[--counts[array[i] - min]] = array[i];
        }
        
        // 将有序数组赋值到array
        for (int i = 0; i < newArray.length; i++) {
            array[i] = newArray[i];
        }

 

◼ 最好、最坏、平均时间复杂度:O(n + k)

◼ 空间复杂度:O(n + k)

◼ k 是整数的取值范围

◼ 属于稳定排序

 

基数排序(Radix Sort)

 

基数排序非常适合用于整数排序(尤其是非负整数),因此本课程只演示对非负整数进行基数排序

◼ 执行流程:依次对个位数、十位数、百位数、千位数、万位数...进行排序(从低位到高位)

◼ 最好、最坏、平均时间复杂度:O(d ∗ (n + k)) ,d 是最大值的位数,k 是进制。属于稳定排序

◼ 空间复杂度:O(n + k),k 是进制

public class Radix_Sort extends Sort {

    public static void main(String[] args) {
        RadixSort();
    }

    protected static void RadixSort() {
        // 找出最大值
        int max = arr[0];
        for (int i = 1; i < arr.length; i++) {
            if (arr[i] > max) {
                max = arr[i];
            }
        }
        
        // 个位数: arr[i] / 1 % 10 = 3
        // 十位数:arr[i] / 10 % 10 = 9
        // 百位数:arr[i] / 100 % 10 = 5
        // 千位数:arr[i] / 1000 % 10 = ...

        for (int divider = 1; divider <= max; divider *= 10) {
            countingSort(divider);
        }
    }
    
    protected static void countingSort(int divider) {
        // 开辟内存空间,存储次数
        int[] counts = new int[10];
        // 统计每个整数出现的次数
        for (int i = 0; i < arr.length; i++) {
            counts[arr[i] / divider % 10]++;
        }
        // 累加次数
        for (int i = 1; i < counts.length; i++) {
            counts[i] += counts[i - 1];
        }
        
        // 从后往前遍历元素,将它放到有序数组中的合适位置
        int[] newarr = new int[arr.length];
        for (int i = arr.length - 1; i >= 0; i--) {
            newarr[--counts[arr[i] / divider % 10]] = arr[i];
        }
        
        // 将有序数组赋值到arr
        for (int i = 0; i < newarr.length; i++) {
            arr[i] = newarr[i];
        }
    }
}

桶排序(Bucket Sort)

 

执行流程

① 创建一定数量的桶(比如用数组、链表作为桶)

② 按照一定的规则(不同类型的数据,规则不同),将序列中的元素均匀分配到对应的桶

③ 分别对每个桶进行单独排序

④ 将所有非空桶的元素合并成有序序列

◼ 元素在桶中的索引

元素值 * 元素数量

image.png

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值