首页 > 编程语言 > 详细

搞懂十大排序算法

时间:2021-03-29 11:43:13      阅读:25      评论:0      收藏:0      [点我收藏+]

算法分类

技术分享图片

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

算法实践

package learning.sort;

import java.lang.reflect.Method;
import java.util.*;

/**
 * 排序算法集合
 * 交换排序:冒泡排序、快速排序、三路切分快速排序
 * 选择排序:简单选择排序、希尔排序
 * 插入排序:直接插入排序、堆排序
 * 归并排序
 * 非比较排序:桶排序、计数排序、基数排序
 */
public class NumberSort {

    /**
     * 交换排序之冒泡排序:相邻的两个值相比较交换位置,把最大值交换到最右边,缩小范围重复上述步骤
     *
     * @param array 数组
     */
    public static void bubbleSort(int[] array) {
        // 是否已排序
        boolean isSorted = false;
        for (int i = 0; i < array.length - 1 && !isSorted; i++) {
            // 默认本次交换后已经有序(如果不发生交换就已经有序了)
            isSorted = true;
            /*
            从下标1开始依次与前一个数比较交换位置
            1、为什么从下标1开始?
            与前一个数比较不会存在越界问题
            2、截止为什么是array.length - i?
            每一次依次相邻比较并交换后会把最大值移到最右边,不用再参与排序
             */
            for (int j = 1; j < array.length - i; j++) {
                // 前一个数较大,交换位置
                if (array[j - 1] > array[j]) {
                    swap(array, j - 1, j);
                    isSorted = false;
                }
            }
        }
    }

    /**
     * 交换排序之快速排序:通过一个切分元素将数组分为两个子数组,左子数组小于等于切分元素,右子数组大于等于切分元素。
     * 通过上述规则不断切分左右子数组,直到各子区间只有一个数为止整个数组就有序了
     *
     * @param array 数组
     */
    public static void quickSort(int[] array) {
        quickSort(array, 0, array.length - 1);
    }

    /**
     * 快排辅助方法
     *
     * @param array 数组
     * @param left  左边界下标
     * @param right 右边界下标
     */
    private static void quickSort(int[] array, int left, int right) {
        // 左边界大于等于右边界中断
        if (left >= right) {
            return;
        }
        // 左边开始下标
        int i = left;
        // 右边开始下标
        int j = right;
        // 基准数,这里取左边第一个数,且作为首个空位
        int base = array[i];
        // 左右边界向中间移动直到相遇
        while (i < j) {
            // 右边开始查找第一个小于基准数的下标
            while (i < j && array[j] >= base) {
                j--;
            }
            // 如果在右边找到小于基准数的值,放在数组左边空位(首个空位是基准数所在下标),下标i向右移动
            if (i < j) {
                // 右边形成一个新的空位array[j]
                array[i++] = array[j];
            }
            // 左边开始查找第一个大于基准数的下标
            while (i < j && array[i] <= base) {
                i++;
            }
            // 如果在左边找到大于基准数的值,放在数组右边空位array[j],下标j向左移动
            if (i < j) {
                // 左边形成一个新的空位array[i]
                array[j--] = array[i];
            }
        }
        // 退出时i等于j,将基准数放入即可
        array[j] = base;
        // 左边子数组重复上述步骤
        quickSort(array, left, i - 1);
        // 右边子数组重复上述步骤
        quickSort(array, i + 1, right);
    }

    /**
     * 三向切分快速排序:将数组分为三部分:小于当前切分元素的部分、等于当前切分元素的部分、大于当前切分元素的部分,中间部分不再切分
     * 好处在于若存在大量重复元素,重复元素总会位于中间部分,不再切分为更小数组,可以在线性时间内完成排序
     *
     * @param array 数组
     */
    public static void threeWayQuickSort(int[] array) {
        threeWayQuickSort(array, 0, array.length - 1);
    }

    /**
     * 三向切分快排辅助方法
     *
     * @param array 数组
     * @param left  左边界下标
     * @param right 右边界下标
     */
    private static void threeWayQuickSort(int[] array, int left, int right) {
        // 左边界大于等于右边界中断
        if (left >= right) {
            return;
        }
        // 基准下标
        int lt = left;
        // 左子数组开始下标
        int i = left + 1;
        // 右子数组开始下标
        int j = right;
        // 基准数
        int base = array[left];
        // 遍历左右下标直到相遇(注意需要包含等于)
        while (i <= j) {
            // 小于基准数放入基准数所在下标,基准数下标右移,相当于放在其左边
            if (array[i] < base) {
                swap(array, lt++, i++);
                // 大于基准数,和右边界数据交换位置,且缩小右边界,左边界下标不变重复第一步
            } else if (array[i] > base) {
                swap(array, i, j--);
                // 等于基准数,左边界右移即可
            } else {
                i++;
            }
        }
        // 小于基准数组成左子数组重复上述步骤
        threeWayQuickSort(array, left, lt - 1);
        // 大于基准数组成右子数组重复上述步骤
        threeWayQuickSort(array, j + 1, right);
    }

    /**
     * 选择排序之简单选择排序:每次选择最小值放在左边有序部分尾部
     *
     * @param array 数组
     */
    public static void selectionSort(int[] array) {
        // 从0~array.length - 2依次默认当前位置为最小值,在右边寻找真实最小值交换位置
        for (int i = 0; i < array.length - 1; i++) {
            // 默认当前下标i为最小值
            int min = i;
            // 在右边寻找真实最小值下标
            for (int j = i + 1; j < array.length; j++) {
                if (array[min] > array[j]) {
                    min = j;
                }
            }
            // 如果最小下标不是当前默认位置,交换位置
            if (i != min) {
                swap(array, i, min);
            }
        }
    }

    /**
     * 选择排序之堆排序:构建数组二叉堆(近似完全二叉树),利用父节点总是小于(大于)或等于任何一个子节点的特性,
     * 根节点总是最大或最小值,摘除根节点加入有序序列,重新构建数组二叉堆重复上述步骤即可
     *
     * @param array 数组
     */
    public static void heapSort(int[] array) {
        int n = 2;
        /*
        构建二叉堆,从最后一个父节点开始
        1、为什么最后一个父节点是array.length / 2 - 1?
        最后一个元素下标i为array.length-1,根据父节点下标公式(i-1)/2得出,其父节点为(array.length-1-1)/2=array.length / 2 - 1
        2、为什么从最后一个父节点开始?
        二叉堆父节点总是大于(小于)或等于任何一个子节点。靠后的父节点也是子节点,从下层开始才能把较大(较小)节点上浮
         */
        for (int i = array.length / n - 1; i >= 0; i--) {
            buildHeap(array, array.length, i);
        }
        /*
        二叉堆已构建,利用根节点总是最大(最小)的特性进行排序,把根节点交换到右侧后缩小数组范围重新构建二叉堆,重复上述步骤
        i表示数组无序部分截止下标,交换到右侧的已经有序了,i缩小到0后整个数组就有序了
         */
        for (int i = array.length - 1; i > 0; i--) {
            // 交换根节点到最右侧
            swap(array, 0, i);
            // 交换后破坏了二叉堆结构,需要重新构建
            buildHeap(array, i, 0);
        }
    }

    /**
     * 构建二叉堆(升序构建最大堆,降序构建最小堆)
     *
     * @param array 数组
     * @param len   数组长度(构建二叉堆部分长度,排序时右侧部分逐渐有序,不参与堆结构构建)
     * @param index 父节点下标
     */
    private static void buildHeap(int[] array, int len, int index) {
        // 父节点下标越界中断
        while (index < len) {
            // 左子节点,右子节点为left+1
            int left = 2 * index + 1;
            // 左子节点下标越界,证明没有子节点不需要调整位置
            if (left >= len) {
                break;
            }
            // 默认左子节点较大
            int max = left;
            // 与右子节点比较,得出最大值下标
            if (left + 1 < len && array[left + 1] > array[max]) {
                max = left + 1;
            }
            // 与父节点比较,若小于父节点不需要交换位置,中断循环
            if (array[max] < array[index]) {
                break;
            }
            // 若大于父节点交换位置
            swap(array, max, index);
            // 交换位置后,破坏了下层最大(小)堆结构,重置父节点进入下次循环
            index = max;
        }
    }

    /**
     * 插入排序之直接插入排序:左边有序,从右边无序节点中依次选择节点,插入到左边合适的位置(从右向左扫描)
     *
     * @param array 数组
     */
    public static void straightInsertSort(int[] array) {
        // 左边默认一个元素且有序,右边无序元素从下标1开始
        for (int i = 1; i < array.length; i++) {
            // 与左边元素比较找到第一个小于的元素位置为止
            for (int j = i; j > 0 && array[j - 1] > array[j]; j--) {
                // 若左边的元素较大,交换位置,重复上述步骤找到合适位置
                swap(array, j, j - 1);
            }
        }
    }


    /**
     * 插入排序之希尔排序:本质上是多次的直接插入排序,通过步长构建新的子序列后进行直接插入排序,缩减步长重复上述步骤,
     * 直到步长为1(步长为1时其实就是直接插入排序),好处在于直接插入排序在数据量小且有序的情况下效率最高
     *
     * @param array 数组
     */
    public static void shellSort(int[] array) {
        // 构建不断缩小的步长直到为1
        int n = 2;
        for (int gap = array.length / n; gap > 0; gap /= n) {
            // 根据步长构建新的子序列进行直接插入排序
            for (int i = gap; i < array.length; i += gap) {
                for (int j = i; j >= gap && array[j - gap] > array[j]; j -= gap) {
                    swap(array, j - gap, j);
                }
            }
        }
    }

    /**
     * 归并排序:利用单个元素的有序性,反向合并两个有序数组
     *
     * @param array 数组
     */
    public static void mergeSort(int[] array) {
        mergeSort(array, 0, array.length - 1);
    }

    /**
     * 把数组拆分为两个子数组分别排序后再合并,子数组的排序利用一个元素的数组是有序实现(不断拆分)
     *
     * @param array 数组
     * @param left  左边界
     * @param right 右边界
     */
    private static void mergeSort(int[] array, int left, int right) {
        // 左右边界未相遇,即只有一个元素(有序)
        if (left < right) {
            // 确认中间分界线
            int mid = (left + right) / 2;
            // 递归左子数组
            mergeSort(array, left, mid);
            // 递归右子数组
            mergeSort(array, mid + 1, right);
            // 合并左右有序数组
            mergeSort(array, left, mid, right);
        }
    }

    /**
     * 合并左右有序数组
     *
     * @param array 数组
     * @param left  左边界
     * @param mid   子数组分界线
     * @param right 右边界
     */
    private static void mergeSort(int[] array, int left, int mid, int right) {
        // 存放排序后数组
        int[] temp = new int[right - left + 1];
        // 左子数组开始下标
        int i = left;
        // 右子数组开始下标
        int j = mid + 1;
        // temp当前下标
        int ix = 0;
        // 从两个数组中选择较小的数放入temp中
        while (i <= mid || j <= right) {
            // 左子数组已经空了,直接把右子数组元素放入temp
            if (i > mid) {
                temp[ix++] = array[j++];
                // 右子数组已经空了,直接把左子数组元素放入temp
            } else if (j > right) {
                temp[ix++] = array[i++];
                // 左右子数组都为空,把较小的数组元素放入temp
            } else if (array[i] < array[j]) {
                temp[ix++] = array[i++];
            } else {
                temp[ix++] = array[j++];
            }
        }
        // 把排序后值放入原数组
        for (int n = left; n <= right; n++) {
            array[n] = temp[n - left];
        }
    }

    /**
     * 桶排序:构建多个有序桶,按照一定规则把数据分到不同的桶里,桶内分别排序(直接插入排序),排序后按照桶顺序回放数据
     *
     * @param array 数组
     */
    public static void bucketSort(int[] array) {
        // 默认最大值
        int maxLen = 1;
        // 遍历原数组得出最大值、最小值
        for (int value : array) {
            int len = String.valueOf(value).length();
            if (len > maxLen) {
                maxLen = len;
            }
        }
        // 创建一个桶集合,相同位数的放入同一个桶
        List<LinkedList<Integer>> buckets = new ArrayList<>(maxLen);
        // 初始化桶并加入到桶集合中,频繁插入选用链表结构LinkedList
        for (int i = 0; i < maxLen; i++) {
            buckets.add(new LinkedList<>());
        }
        // 遍历原数据均匀放入对应的桶中
        for (int v : array) {
            // 计算数据对应的桶,根据实际场景设计,保证数据可以均匀分布且桶之间数据有序
            int ix = String.valueOf(v).length() - 1;
            // 有序插入到桶中
            bucketInsert(buckets.get(ix), v);
        }
        // 将桶中元素全部取出插入的原数组中
        int ix = 0;
        for (LinkedList<Integer> bucket : buckets) {
            for (int v : bucket) {
                array[ix++] = v;
            }
        }
    }

    /**
     * 辅助桶排序:有序插入元素
     *
     * @param bucket 桶
     * @param data   待插入数据
     */
    private static void bucketInsert(List<Integer> bucket, int data) {
        // 是否插入元素
        boolean insertFlag = true;
        // 使用迭代器遍历桶
        ListIterator<Integer> it = bucket.listIterator();
        while (it.hasNext()) {
            // 如果待插入数据小于等于当前元素,就是需要插入的位置
            if (data <= it.next()) {
                // 迭代器位置偏移回上一个位置
                it.previous();
                // 位置偏移后插入元素,刚好就在本次比较元素之前
                it.add(data);
                // 元素已插入修改插入标志
                insertFlag = false;
                // 中断循环
                break;
            }
        }
        // 如果迭代过程没有插入说明待插入数据最大,插入到末尾
        if (insertFlag) {
            bucket.add(data);
        }
    }

    /**
     * 计数排序:本质上是一个特殊的桶排序,当桶的个数最大的时候,即相同的数一个桶,就是计数排序
     * 创建一个长度为maxValue-minValue+1的数组计数,如下:
     * 扫描一遍原始数组,以当前值- minValue 作为下标,将该下标的计数器增1
     * 扫描一遍计数器数组,按顺序把值放回原数组
     *
     *
     * @param array 数组
     */
    public static void countingSort(int[] array) {
        // 默认最小值
        int min = array[0];
        // 默认最大值
        int max = array[0];
        // 遍历原数组得出最大值、最小值
        for (int i = 1; i < array.length; i++) {
            if (array[i] < min) {
                min = array[i];
            } else if (array[i] > max) {
                max = array[i];
            }
        }
        // 定义一个计数数组
        int[] count = new int[max - min + 1];
        // 遍历原数组统计数据出现次数,注意下标需要用当前值- minValue,因为数组长度为max - min + 1
        for (int v : array) {
            count[v - min]++;
        }
        // 移动下标
        int ix = 0;
        // 遍历计数数组回放原数组
        for (int i = 0; i < count.length; i++) {
            // 出现次数,对应值为i + min
            int cnt = count[i];
            // 根据出现次数循环多次放入原数组
            while (cnt-- > 0) {
                array[ix++] = i + min;
            }
        }
    }

    /**
     * 基数排序:构建有限的有序桶,把数据分类放入,更换分类指标重复上述步骤
     * 例如:12 8 33 16,构建0~9号桶
     * 第一步:低位作为指标分别放入2、3、6、8号桶,得出:12 33 16 8
     * 第二步:高一位作为指标分别放入0、1、3号桶,得出:8 12 16 33,注意不要破坏低位的顺序
     *
     * @param array 数组
     */
    public static void radixSort(int[] array) {
        // 基数数组,数字0~9,长度10即可
        int[] radix = new int[10];
        // 排序后临时存放数组
        int[] temp = new int[array.length];
        // 不确定总共有多少位,用于统计信息判断是否中断循环
        int cnt = 0;
        // 用于去除低位的除数,1 10 100 ...
        int m = 1;
        // 从低位到高位进行排序
        while (true) {
            // 重置桶中上次统计的数据
            for (int i = 0; i < radix.length; i++) {
                radix[i] = 0;
            }
            // 统计当前位对应值出现的次数
            for (int d : array) {
                // 去除当前位右边的低位,m为除数,每迭代一次*10
                int v = d / m;
                // 统计数组中含有指定位数的数量
                cnt = v > 1 ? cnt + 1 : cnt;
                // 当前位对应值出现次数计数
                radix[v % 10]++;
            }
            // 不存在指定位数的数,即没有更高的位了,中断循环
            if (cnt == 0) {
                break;
            }
            // 通过相邻的两个计数相加,得出基数对应的右边界(使用时需要减一)
            for (int i = 1; i < radix.length; i++) {
                radix[i] += radix[i - 1];
            }
            /*
            把原数组中数据按照当前位依次放入临时数组中
            1、相同位数据之间无序,不同位数据之间有序
            2、必须倒序遍历原数组,可以保证高位排序后低位有序,因为是从右边界开始放入,例如 45 35 36 46,从右边取低位较大
             */
            for (int i = array.length - 1; i >= 0; i--) {
                // 当前位对应值
                int ix = array[i] / m % 10;
                // --radix[ix]为右边界下标,从右边界开始放入
                temp[--radix[ix]] = array[i];
            }
            // 把部分有序的数据拷贝到原数组,进入高一位排序
            for (int i = 0; i < temp.length; i++) {
                array[i] = temp[i];
            }
            // 重置计数
            cnt = 0;
            // 高一位除数*10
            m *= 10;
        }
    }

    /**
     * 数组指定位置交换值(一个数异或同一个数两次,结果还是那个数)
     * 注意:下标i、j相同不能使用异或交换
     *
     * @param array 数组
     * @param i     交换位置i
     * @param j     交换位置j
     */
    private static void swap(int[] array, int i, int j) {
        if (i != j) {
            array[i] = array[i] ^ array[j];
            // 相当于array[i] ^ array[j] ^ array[j]
            array[j] = array[i] ^ array[j];
            // 相当于array[i] ^ array[j] ^ array[i]
            array[i] = array[i] ^ array[j];
        }
    }

    /**
     * 打印数组元素
     *
     * @param array    数组
     * @param sortType 排序类型
     */
    private static void display(int[] array, String sortType) {
        Arrays.stream(array).forEach(x -> System.out.print(x + " "));
        System.out.println(sortType);
    }

    /**
     * 通过反射调用指定排序方法
     *
     * @param sortType 排序方法
     */
    public static void sort(String sortType) {
        try {
            int[] array = new int[]{11, 56, 35, 62, 97, 21, 36, 33, 86, 81, 35, 8, 135, 235, 4567};
            Class<?> clz = Class.forName(NumberSort.class.getName());
            Method m = clz.getDeclaredMethod(sortType, int[].class);
            m.invoke(null, new Object[]{array});
            display(array, sortType);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        sort("bubbleSort");
        sort("quickSort");
        sort("threeWayQuickSort");
        sort("selectionSort");
        sort("heapSort");
        sort("straightInsertSort");
        sort("shellSort");
        sort("mergeSort");
        sort("bucketSort");
        sort("countingSort");
        sort("radixSort");
    }
}

选择准则

影响排序的因素有很多,平均时间复杂度低的算法并不一定就是最优的。相反,有时平均时间复杂度高的算法可能更适合某些特殊情况。同时,选择算法时还得考虑它的可读性,以利于软件的维护。一般而言,需要考虑的因素有以下四点:

  • 待排序的记录数目n的大小
  • 记录本身数据量的大小,也就是记录中除关键字外的其他信息量的大小(复杂对象按局部属性排序)
  • 关键字的结构及其分布情况
  • 对排序稳定性的要求

根据待排序元素的个数n,分析如下:

  • 当n较大,则应采用时间复杂度为${O(nlog_2n)}$的排序方法
    • 快速排序:是目前基于比较的内部排序中被认为最好的算法,当待排序的关键字是随机分布时,快速排序的平均时间最短
    • 堆排序 :所需辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况
    • 归并排序:若需要排序稳定,且空间允许,可选用归并排序,但从单个记录起进行两两归并并不提倡,一般建议与直接插入排序(稳定)结合使用,先利用直接插入排序求得较长的有序子文件,再两两归并
  • 当n较小,可采用直接插入或直接选择排序
    • 直接插入排序:当元素分布有序,直接插入排序将大大减少比较次数和移动记录的次数
    • 简单选择排序:如果不要求稳定性,可选择简单选择排序
  • 一般不使用或不直接使用传统的冒泡排序
  • 基数排序:它是一种稳定的排序算法,但有一定的局限性
    • 关键字可分解
    • 记录的关键字位数较少,如果密集更好
    • 如果是数字时,最好是无符号的,否则将增加相应的映射复杂度,可先将其正负分开排序

搞懂十大排序算法

原文:https://www.cnblogs.com/sheung/p/14591411.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!