算法刻意练习之排序算法

1 经典算法对比

1.1 算法分类

在这里插入图片描述
在这里插入图片描述

1.2 算法复杂度

在这里插入图片描述

1.3 相关概念

(1)稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面
(2)不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面
(3)时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
(4)空间复杂度:是指算法在计算机

1.4 参考链接

十大经典排序算法(动图演示)

912. 排序数组

在线动画演示各种排序算法过程 - aTool在线工具

2 初级排序

在这里插入图片描述

2.1 选择排序(了解)

在这里插入图片描述
在这里插入图片描述

/**
     * 选择排序
     * 思想:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置;
     *      然后,再从剩余未排序元素中继续寻找最小(大)元素;最后放到 前面已排序数组的 末尾。
     *
     * 时间复杂度:O(n^2),这里n是数组的长度;
     * 空间复杂度:O(1),使用到常数个临时变量。
     *
     * @param array
     * @return
     */
    fun selectSort(array: IntArray) : IntArray {
        if (array.size <= 1) return array

        // i = 1 逐步递增,是指 前面已排序数组的 最后一个位置
        for (i in array.indices) {
            var minIndex = i
            // 从下一个位置后,找到最小数的下标
            for (j in i + 1 until array.size) {
                // 将最小数的索引保存
                if (array[j] < array[minIndex]) minIndex = j
            }

            // 最后交换元素
            val temp = array[i]
            array[i] = array[minIndex]
            array[minIndex] = temp
        }

        return array
    }

2.2 插入排序(了解)

在这里插入图片描述
在这里插入图片描述

/**
     * 插入排序
     * 
     * 思想:将数组中的数据分为两个区间,已排序区间和未排序区间。初始已排序区间只有一个元素, 就是数组的第一个元素。
     * 核心思想是:从1到n,取未排序区间中的第一个元素,在已排序区间中找到合适的插入位置将其插入,
     * 并保证已排序区间数据一直有序。重复这个过程,直到未排序区间中元素为空,算法结束。
     *
     * 时间复杂度:O(n^2),这里n是数组的长度;
     * 空间复杂度:O(1),使用到常数个临时变量。
     *
     * @param array
     * @return
     */
    fun insertSort(array: IntArray) : IntArray {
        if (array.size <= 1) return array

        // i 从 1 到 n,是指 未排序区间中的第一个元素
        for (i in 1 until array.size) {
            // 先临时暂存这个变量
            val temp = array[i]
            var pre = i - 1
            // 然后前面比插入元素大的值逐个后移,空出一个位置
            while(pre >= 0 && temp < array[pre]) {
                array[pre + 1] = array[pre]
                pre--
            }

            // 最后把「临时变量」插入到空位,并保证已排序区间数据一直有序
            array[pre + 1] = temp
        }

        return array
    }

2.3 冒泡排序(了解)

在这里插入图片描述

/**
     * 冒泡排序  超时
     * 思想:将数组中的数据分为两个区间,未排序区间和已排序区间。
     * 
     * 操作相邻的两个数据。每次冒泡操作都会对相邻的两个元素进行比较,看大小是否相等,如果不相等就让它俩互换;
     * 一次冒泡,进行 n-1 趟比较并交换,会让至少一个元素移动到已排序区间的第一个位置;
     * 重复 n 次,就完成了 n 个数据的排序工作。
     *
     * 时间复杂度:O(n^2),这里n是数组的长度;
     * 空间复杂度:O(1),使用到常数个临时变量。
     *
     * @param array
     * @return
     */
    fun bubbleSort(array: IntArray): IntArray {
        if (array.size <= 1) return array

        // array.size - 1 逐步递减,是指 后面已排序数组 前一个位置
        for (i in array.size - 1 downTo 0) {
            var sorted = true
            for (j in 0 until i) {
                // 相邻元素两两对比
                if (array[j] > array[j + 1]) {
                    // 元素交换
                    val temp = array[j]
                    array[j] = array[j + 1]
                    array[j + 1] = temp

                    sorted = false
                }
            }
            if (sorted) break
        }

        return array
    }

3 高级排序

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.1 快速排序(重点)

在这里插入图片描述
在这里插入图片描述

/**
     * 快速排序     9, 8, 6, 3, 2, 4
     *
     * 基本思路:分治思想,通过一趟排序划分数组,将排序的数据分割成独立的两部分,分在新的基准位置左右;
     *          然后递归地去排序它左边的部分(比它小的值)和右边的部分(比它大的值),依次进行下去,直到数组有序;
     *
     * 对比:归并排序的处理过程是由下到上的,先处理子问题,然后再合并。而快排正好相反,它的处理过程是由上到下的,先分区,然后再处理子问题。
     *
     * 时间复杂度:O(nlogn)。
     * 空间复杂度:O(logn)。
     *
     * @param array
     * @param begin
     * @param end
     * @return
     */
    fun quickSort(array: IntArray, begin: Int, end: Int) {
        if (end <= begin) return

        // 获取分区点
        val q = partition(array, begin, end)

        // 对两个子序列左边进行快排,直到序列为空或者只有一个元素
        quickSort(array, begin, q - 1)

        // 对两个子序列右边进行快排
        quickSort(array, q + 1, end)
    }
   /**
     * 划分数组:
     * 定义 counter 是从 begin 到 end 元素的位置,最初以 end 作为临时基准位置;
     * 如果存在比 end 的值小的元素,都和 counter 交换,然后+1;遍历结束以后,将 counter 和 end 元素交换,成为新的基准位置;
     * 这样,排序的数据分割成独立的两部分,分在新的基准位置左右。
     *
     * @param array
     * @param begin
     * @param end 最初的基准位置
     * @return
     */
    private fun partition(array: IntArray, begin: Int, end: Int): Int {
        // 定义 counter 是从 begin 到 end 元素的位置,最初以 end 作为分区点
        var counter = begin
        // 如果存在比 end 的值小的元素,都和 counter 交换,然后+1
        for (i in begin until end) {
            if (array[i] < array[end]) {
                val temp = array[counter]
                array[counter] = array[i]
                array[i] = temp
                counter++
            }
        }

        // 遍历结束以后,将 pivot 和 end 元素交换,成为新的分区点
        val temp = array[end]
        array[end] = array[counter]
        array[counter] = temp
        return counter
    }

在这里插入图片描述

3.2 归并排序(重点)

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

/**
     * 归并排序(Merge Sort)  采用分治法的一个非常典型的应用
     * 思路:1.把长度为n的输入序列分成两个长度为n/2的子序列;
     *      2.对这两个子序列分别采用归并排序;
     *      3.将两个排序好的子序列合并成一个最终的排序序列。
     *
     * 时间复杂度:O(n * log n),这里n是数组的长度:
     * 空间复杂度:O(n),辅助数组与输入数组规模相当。
     *
     * @param array
     * @param left
     * @param right
     */
    fun mergeSort(array: IntArray, left: Int, right: Int) {
        if (right <= left) return

        val mid = (left + right) shr 1 // 右移 (left + right) / 2

        mergeSort(array, left, mid)
        mergeSort(array, mid + 1, right)

        merge(array, left, mid, right)
    }

    fun merge(array: IntArray, left: Int, mid: Int, right: Int) {
        // 临时数组
        val temp = IntArray(right - left + 1)
        var i = left
        var j = mid + 1
        var k = 0

        // 比较两个小数组相应下标位置的数组大小,小的先放进缓存数组
        while (i <= mid && j <= right) {
            temp[k++] = if (array[i] <= array[j]) array[i++] else array[j++]
        }

        // 如果左边还有数据需要拷贝,把左边数组剩下的拷贝到缓存数组
        while (i <= mid) temp[k++] = array[i++]
        // 如果右边还有数据需要拷贝,把左边数组剩下的拷贝到缓存数组
        while (j <= right) temp[k++] = array[j++]
        // 将缓存数组中的内容复制回原数组
        for (p in temp.indices) {
            array[left + p] = temp[p]
        }
    }

3.3 堆排序(重点)算法刻意练习之堆、二叉堆

在这里插入图片描述

/**
     * 堆排序(heap sort)
     *
     * 核心思想:先将待排序的序列建成大根堆,使得每个父节点的元素大于等于它的子节点。此时整个序列最大值即为堆顶元素,
     *          我们将其与末尾元素交换,使末尾元素为最大值,然后再调整堆顶元素使得剩下的 n-1n−1 个元素仍为大根堆,再重复执行以上操作我们即能得到一个有序的序列
     *
     * 时间复杂度:O(nlogn)。初始化建堆的时间复杂度为O(n),建完堆以后需要进行n−1次调整,一次调整(即maxHeapify) 的时间复杂度为O(logn),
     *                     那么n−1次调整即需要O(nlogn)的时间复杂度。因此,总时间复杂度为O(n+nlogn)=O(nlogn);
     * 空间复杂度:O(1)。只需要常数的空间存放若干变量。
     *
     * @param array
     */
    fun heapSort(array: IntArray) : IntArray {
        if (array.size <= 1) return array

        // 建立最大堆:遍历父节点,索引为的父结点的索引是(i-1)/2,假设有5个
        for (i in (array.size - 1) / 2 downTo 0) {
            // 调整大堆,只需遍历 i = 1、0
            maxHeap(array, array.size, i)
        }

        // 排序:每次忽略最后一个最大的值,假设有5个,i = 4、3、2、1
        for (i in array.size - 1 downTo 1) {
            // 最大的在0位置,那么开始沉降,这样每交换一次最大的值就丢到最后了
            val temp = array[0]
            array[0] = array[i]
            array[i] = temp
            // 调整大堆
            maxHeap(array, i, 0)
        }

        return array
    }

    /**
     * 调整大堆
     *
     * @param array 堆数组
     * @param length 表示用于构造大堆的数组长度元素数量
     * @param index 从哪位置开始
     */
    private fun maxHeap(array: IntArray, length: Int, index: Int) {
        // 左/右节点:索引为i的左孩子的索引是(2*i+1),索引为i的左孩子的索引是(2*i+2)
        val left = index * 2 + 1
        val right = index * 2 + 2
        // 目标序号:父节点,largest == 变化的值
        var largest = index

        // left < length。左节点大于根节点,将左序号赋值为目标序号
        if (left < length && array[left] > array[index]) largest = left

        // 右节点大于根节点(新目标序号),将右序号赋值为目标序号
        if (right < length && array[right] > array[largest]) largest = right

        // 目标序号元素不是最大值
        if (index != largest) {
            // 数据交换
            val temp = array[index]
            array[index] = array[largest]
            array[largest] = temp

            // 继续调整大堆
            maxHeap(array, length, largest)
        }
    }

4 特殊排序

在这里插入图片描述

4.1 基数排序

4.1.1 核心思想

基本思想是:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。

3.9.2 演示图

这里写图片描述
(1)个位—把个位的数值排序好:
这里写图片描述
(2)十位—把十位的数值排序好:
这里写图片描述
(3)百位—把百位的数值排序好:
这里写图片描述

3.9.3 源码

package com.dn.sort;
public class BasicSort {
    public void basicSort(int[] array) {
        int max = 0;// 获取最大值
        int digit = 10;//0-9
        int times = 0;// 获取最大值位数

        for (int num : array) {
            if (max < num) {
                max = num;
            }
        }
        while (max > 0) {
            max = max / 10;
            times++;
        }

        //建立10个集合(0-9)
        List<ArrayList> baseList = new ArrayList<ArrayList>();// 多维数组
        for (int i = 0; i < digit; i++) {
            ArrayList list1 = new ArrayList<>();
            baseList.add(list1);
        }

        //进行times次分配和收集;
        for (int i = 0; i < times; i++) {
            //分配数组元素
            for (int j = 0; j < array.length; j++) {
                // 获取对应的位的值(pow是平方,i为0是个位,i为1是10位,i为2是百位)
                int x = array[j] % (int) Math.pow(10, i + 1) / (int) Math.pow(10, i);//取余、取整
                ArrayList list2 = baseList.get(x);
                list2.add(array[j]);// 把元素添加进对应下标数组
            }

            int count = 0;//元素计数器;
            //收集队列元素;
            for (int j = 0; j < digit; j++) {//0-9
                while (baseList.get(j).size() > 0) {
                    ArrayList<Integer> list3 = baseList.get(j);// 拿到每一个集合
                    array[count] = list3.get(0);//获取集合第一个,每次都是新的数据
                    list3.remove(0);//删除分配给集合的数据
                    count++;
                }
            }
        }
    }
    
    public static void main(String[] args) {
        BasicSort basicSort = new BasicSort();
        int[] a = { 136, 2, 6, 8, 9, 2, 8, 11, 23, 56, 34, 90, 89, 29, 145,209, 320, 78, 3 };
        basicSort.basicSort(a);
        for (int n : a) {
            System.out.print(" " + n);
        }
    }
}

4.2 桶排序

3.10.1 背景

给阿里 2 万多名员工按年龄排序应该选择哪个算法?阿里员工特征,2万人,规模较小;处于18-99,尤其是23-40占了大部分,使用桶排序适合。

3.10.2 核心思想

基本思想是:桶排序(Bucket Sort)的原理很简单,它是将数组分到有限数量的桶子里。
  过程:假设待排序的数组a中共有N个整数,并且已知数组a中数据的范围[0, MAX)。在桶排序时,创建容量为MAX的桶数组r,并将桶数组元素都初始化为0;将容量为MAX的桶数组中的每一个单元都看作一个"桶"。在排序时,逐个遍历数组a,将数组a的值,作为"桶数组r"的下标。当a中数据被读取时,就将桶的值加1。例如,读取到数组a[3]=5,则将r[5]的值+1。
  特征:(1)桶排序是稳定的; (2)桶排序是常见排序算法中最快的一种,大多数情况下比快排和归并排序还要快 (3)桶排序非常快但是也非常消耗空间,典型的以空间换时间,基本上是最耗内存的一种排序算法。

3.10.3 演示图

bucketSort(a, n, max)是作用是对数组a进行桶排序,n是数组a的长度,max是数组中最大元素所属的范围[0,max)。假设a={8,2,3,4,3,6,6,3,9}, max=10。此时,将数组a的所有数据都放到需要为0-9的桶中。如下图:
这里写图片描述

3.10.4 源码

public class BucketSort {
    /*
     * 桶排序
     * 参数说明:
     *     a -- 待排序数组
     *     max -- 数组a中最大值的范围
     */
    public static void bucketSort(int[] a, int max) {
        int[] buckets;

        if (a==null || max<1)
            return ;

        // 创建一个容量为max的数组buckets,并且将buckets中的所有数据都初始化为0。
        buckets = new int[max];

        // 1. 计数
        for(int i = 0; i < a.length; i++) 
            buckets[a[i]]++; 

        // 2. 排序
        for (int i = 0, j = 0; i < max; i++) {
            while( (buckets[i]--) >0 ) {
                a[j++] = i;
            }
        }

        buckets = null;
    }

    public static void main(String[] args) {
        int i;
        int a[] = {8,2,3,4,3,6,6,3,9};

        System.out.printf("before sort:");
        for (i=0; i<a.length; i++)
            System.out.printf("%d ", a[i]);
    
        bucketSort(a, 10); // 桶排序

        System.out.printf("after  sort:");
        for (i=0; i<a.length; i++)
            System.out.printf("%d ", a[i]);
    }
}
before sort:8 2 3 4 3 6 6 3 9 
after  sort:2 3 3 3 4 6 6 8 9 

3.10.5 参考链接

桶排序

[排序算法]–桶排序的Java实现

Java实现桶排序

5 总结

这里写图片描述
这里写图片描述
图片来源于:八大经典排序算法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值