• 排序算法总结


    排序大的分类可以分为两种:内排序和外排序。在排序过程中,全部记录存放在内存,则称为内排序。如果排序过程中需要使用外存,则称为外排序。下面讲的排序都是属于内排序。 


    一、插入排序

    (一)、直接插入排序

    public class Sort {
        public static void main(String[] args) {
            int[] arr = {1, 3, 51, 65, 6, 34, 67, 343, 56};
            int[] sortResult = insertionSort(arr);
            System.out.println(Arrays.toString(sortResult));
        }
    
        /**
         * 用一个临时变量存储待插入的值,从后往前找,如果找到比这个值大的元素,则将其前面的元素依次后移,
         * 结束后再将带插入的值放到该插入的位置,减去了许多不必要的交换操作
         * @param arr
         * @return
         */
        public static int[] insertionSort(int[] arr) {
            for (int i = 1; i < arr.length; i++) {
                //待插入元素
                int insertValue = arr[i];
                int preIndex = i - 1;
                while (preIndex >= 0 && insertValue < arr[preIndex]) {
                    arr[preIndex + 1] = arr[preIndex];
                    preIndex--;
                }
                arr[preIndex + 1] = insertValue;
            }
            return arr;
        }
    }

    (二)、希尔排序(shell排序)

    public class Sort {
        public static void main(String[] args) {
            int[] arr = {1, 3, 51, 65, 6, 34, 67, 343, 56};
            int[] sortResult = shellSort(arr);
            System.out.println(Arrays.toString(sortResult));
        }
    
        /**
         * 希尔排序
         * 插入排序的升级版,设定步长为数组长度的一半,每次都除以二,直到步长为1
         * @param arr
         * @return
         */
        public static int[] shellSort(int[] arr) {
            int len = arr.length;
            for (int k = len / 2; k > 0; k /= 2) {
                for (int i = k; i < len; i++) {
                    int temp = arr[i];
                    int j = i - k;
                    while (j >= 0 && temp < arr[j]) {
                        arr[j + k] = arr[j];
                        j -= k;
                    }
                    arr[j + k] = temp;
                }
            }
            return arr;
        }
    }

    二、选择排序

    (一)、简单选择排序(直接排序)

    public class Sort {
        public static void main(String[] args) {
            int[] arr = {1, 3, 51, 65, 6, 34, 67, 343, 56};
            int[] sortResult = selectionSort(arr);
            System.out.println(Arrays.toString(sortResult));
        }
    
        /**
         * 遍历数组,每次遍历都找到最小的元素,记录其下标,内层循环结束后再根据下标将其与数组头部元素交换
         * 与冒泡排序不同的是,冒泡排序每次循环可能交换多次,而选择排序最多交换一次
         * @param arr 待排序数组
         * @return
         */
        public static int[] selectionSort(int[] arr) {
            int temp, minIndex;
            for (int i = 0; i < arr.length - 1; i++) {
                minIndex = i;
                for (int j = i + 1; j < arr.length; j++) {
                    if (arr[j] < arr[minIndex]) {
                        minIndex = j;
                    }
                }
                temp = arr[minIndex];
                arr[minIndex] = arr[i];
                arr[i] = temp;
            }
            return arr;
        }
    }

    (二)、堆排序

    public class HeapSort {
    
        public static void main(String[] args) {
            int[] arr = {1, 3, 51, 65, 6, 34, 67, 343, 56};
            int[] sortResult = heapSort(arr);
            System.out.println(Arrays.toString(sortResult));
        }
    
        public static int[] heapSort(int[] arr) {
            //以最后一个非叶子结点构建大顶堆
            for (int i = arr.length / 2 - 1; i >= 0; i--) {
                adjustHeap(arr, i, arr.length);
            }
            //此时顶部元素是最大的,交换顶部元素和末端元素
            for (int i = arr.length - 1; i > 0; i--) {
                swap(arr, 0, i);
                //末端元素已经是最大的了,无需考虑排序
                adjustHeap(arr, 0, i);
            }
            return arr;
        }
    
        /**
         * 形成大顶堆
         *
         * @param arr 数组元素
         * @param i   当前结点位置
         * @param len 结点个数
         */
        public static void adjustHeap(int[] arr, int i, int len) {
            //保存当前结点
            int temp = arr[i];
            //遍历当前结点的左子结点
            for (int k = 2 * i + 1; k < len; k = 2 * k + 1) {
                //如果右结点存在 且 右结点比左结点大,指向右结点
                if (k + 1 < len && arr[k] < arr[k + 1]) {
                    k++;
                }
                //判断当前结点和左(右)结点哪个大
                if (temp < arr[k]) {
                    //交换
                    swap(arr, k, i);
                    //交换后,下次遍历以该子结点作为根节点的子树就会受到影响,因此需要重新指定下次的根节点
                    i = k;
                } else {
                    //不用交换,直接终止循环
                    break;
                }
            }
        }
    
        public static void swap(int[] arr, int i, int j) {
            int temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
        }
    }

    三、交换排序

    (一)、冒泡排序

    /**
     * 遍历数组,依次比较相邻的元素并交换,每次都将最大元素(根据正序还是逆序决定)放到数组末尾
     * @param arr 待排序数组
     * @return
     */
    public static int[] bubbleSort(int[] arr) {
        int temp;
    
        for (int i = 0; i < arr.length - 1; i++) {
            for (int j = 0; j < arr.length - i - 1; j++) {
                if (arr[j] > arr[j + 1]) {
                    temp = arr[j + 1];
                    arr[j + 1] = arr[j];
                    arr[j] = temp;
                }
            }
        }
        return arr;
    }

    冒泡排序优化一:

    //冒泡排序:优化一
    public static int[] bubbleSort1(int[] arr) {
        int temp;
        //记录数组是否有序
        boolean isSorted;
    
        for (int i = 0; i < arr.length - 1; i++) {
            isSorted = true;
            for (int j = 0; j < arr.length - i - 1; j++) {
                if (arr[j] > arr[j + 1]) {
                    temp = arr[j + 1];
                    arr[j + 1] = arr[j];
                    arr[j] = temp;
                    //记录本轮排序是否交换了元素,如果交换则置为false
                    isSorted = false;
                }
            }
            //没有交换证明已经是有序的了,直接终止循环
            if (isSorted) {
                break;
            }
        }
        return arr;
    }

    冒泡排序优化二:

    //冒泡排序:优化二
    public static int[] bubbleSort2(int[] arr) {
        int temp;
        boolean isSorted;
        //第一次循环边界
        int sortBorder = arr.length - 1;
        //记录每轮排序最后一次进行交换的位置
        int lastSwapIndex = 0;
    
        for (int i = 0; i < arr.length - 1; i++) {
            isSorted = true;
            for (int j = 0; j < sortBorder; j++) {
                if (arr[j] > arr[j + 1]) {
                    temp = arr[j + 1];
                    arr[j + 1] = arr[j];
                    arr[j] = temp;
                    isSorted = false;
                    lastSwapIndex = j;
                }
            }
            sortBorder = lastSwapIndex;
            if (isSorted) {
                break;
            }
        }
        return arr;
    }

    (二)、快速排序

    算法原理:先在数组中选择一个数字,通过一趟排序将要排序的数据分割成独立的两部分:比选择的数字小的数字都移到数组的左边,比选择的数字大的数字都移到数组的右边。然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

    快速排序中基准的选取:
    1. 第一个元素或最后一个元素,但是当数组中的元素原本为逆序时,升序排序就会变成一次只能确定基准元素的位置,无法发挥快排的优势
    2. 随机选取
    3. 三数取中法,也就是取左端、中间、右端三个数,然后进行排序,将中间数作为枢纽值。

    举个栗子:

    原数组:{3,7,2,9,1,4,6,8,10,5}
    期望结果:{1,2,3,4,5,6,7,8,9,10}

    快速排序示意图:

    代码实现:

    public class QuickSort {
        
        public static int divide(int[] a, int start, int end){
            //每次都以最右边的元素作为基准值
            int base = a[end];
            //start一旦等于end,就说明左右两个指针合并到了同一位置,可以结束此轮循环。
            while(start < end){
                while(start < end && a[start] <= base)
                    //从左边开始遍历,如果比基准值小,就继续向右走
                    start++;
                //上面的while循环结束时,就说明当前的a[start]的值比基准值大,应与基准值进行交换
                if(start < end){
                    //交换
                    int temp = a[start];
                    a[start] = a[end];
                    a[end] = temp;
                    //交换后,此时的那个被调换的值也同时调到了正确的位置(基准值右边),因此右边也要同时向前移动一位
                    end--;
                }    
                while(start < end && a[end] >= base)
                    //从右边开始遍历,如果比基准值大,就继续向左走
                    end--;
                //上面的while循环结束时,就说明当前的a[end]的值比基准值小,应与基准值进行交换
                if(start < end){
                    //交换
                    int temp = a[start];
                    a[start] = a[end];
                    a[end] = temp;
                    //交换后,此时的那个被调换的值也同时调到了正确的位置(基准值左边),因此左边也要同时向后移动一位
                    start++;
                }    
                
            }
            //这里返回start或者end皆可,此时的start和end都为基准值所在的位置
            return end;
        }
     
        /**
         * 排序
         * @param a
         * @param start
         * @param end
         */
        public static void sort(int[] a, int start, int end){
            if(start > end){ //start >= end ?
                //如果只有一个元素,就不用再排下去了
                return;
            } 
            else{
                //如果不止一个元素,继续划分两边递归排序下去
                int partition = divide(a, start, end);
                sort(a, start, partition-1);
                sort(a, partition+1, end);
            }
                
        }
    
    
        public static void main(String[] args) {
            // TODO Auto-generated method stub
            int[] a = new int[]{2,7,4,5,10,1,9,3,8,6};
            int[] b = new int[]{1,2,3,4,5,6,7,8,9,10};
            int[] c = new int[]{10,9,8,7,6,5,4,3,2,1};
            int[] d = new int[]{1,10,2,9,3,2,4,7,5,6};
            int[] e = {3};
                
            sort(b, 0, b.length-1);
                
            System.out.println("排序后的结果:");
            for(int x : b){
                System.out.print(x+" ");
            }
    
        }
    
    }

    四、归并排序

    public class Sort {
        public static void main(String[] args) {
            int[] arr = {1, 3, 51, 65, 6, 34, 67, 343, 56};
            int[] sortResult = mergeSort(arr);
            System.out.println(Arrays.toString(sortResult));
        }
    
        //归并排序
        public static int[] mergeSort(int[] arr) {
            return mergeSort(arr, 0, arr.length - 1, new int[arr.length]);
        }
    
        /**
         * 归并排序通过递归将数组分解为只有两个元素,按照它们的大小放入到一个临时数组中,直到全部合并
         *
         * @param arr   待排序数组
         * @param left  左索引
         * @param right 右索引
         * @param temp  临时数组,存储每次合并后的元素
         * @return
         */
        public static int[] mergeSort(int[] arr, int left, int right, int[] temp) {
            if (left < right) {
                int mid = (left + right) >> 1;
                //向左分解
                mergeSort(arr, left, mid, temp);
                //向右分解
                mergeSort(arr, mid + 1, right, temp);
                //合并
                merge(arr, left, mid, right, temp);
            }
            return arr;
        }
    
        //合并
        public static int[] merge(int[] arr, int left, int mid, int right, int[] temp) {
            int i = left, j = mid + 1, k = 0;
            //按大小放入临时数组中
            while (i <= mid && j <= right) {
                if (arr[i] < arr[j]) {
                    temp[k] = arr[i];
                    k++;
                    i++;
                } else {
                    temp[k] = arr[j];
                    k++;
                    j++;
                }
            }
    
            //将剩余元素放到temp剩余位置
            while (i <= mid) {
                temp[k] = arr[i];
                k++;
                i++;
            }
            while (j <= right) {
                temp[k] = arr[j];
                k++;
                j++;
            }
    
            //将排好序的temp数组元素赋值给原数组
            k = 0;
            int l = left;
            while (l <= right) {
                arr[l] = temp[k];
                k++;
                l++;
            }
    
            return arr;
        }
    
    }

    五、基数排序

    public class RadixSort {
    
        public static void main(String[] args) {
            int[] arr = {1, 3, 51, 65, 6, 34, 67, 343, 56};
            int[] sortResult = radixSort(arr);
            System.out.println(Arrays.toString(sortResult));
        }
    
        /**
         * 基数排序:
         * 根据每个数的个位、十位、百位...的值(0~9)放入桶中(规则和计数排序相同),因此需要10个桶
         *
         * @param arr
         * @return
         */
        public static int[] radixSort(int[] arr) {
            //创建并初始化10个桶
            ArrayList<LinkedList<Integer>> bucketList = new ArrayList<>(10);
            for (int i = 0; i < 10; i++) {
                bucketList.add(new LinkedList<>());
            }
    
            //找出数据中最大值
            int max = arr[0];
            for (int i = 1; i < arr.length; i++) {
                if (max < arr[i]) {
                    max = arr[i];
                }
            }
    
            //获取最大值的位数
            int maxRadix = (max + "").length();
            //从个位开始
            for (int i = 0; i < maxRadix; i++) {
                //将待排序元素放入桶中
                for (int j = 0; j < arr.length; j++) {
                    //获取数字对应位上的值
                    int radix = arr[j] / (int) Math.pow(10, i) % 10;
                    //放入对应的桶中
                    bucketList.get(radix).add(arr[j]);
                }
                //将桶中元素放回原数组
                int k = 0;
                for (int j = 0; j < 10; j++) {
                    for (Integer number : bucketList.get(j)) {
                        arr[k++] = number;
                    }
                    bucketList.get(j).clear();
                }
            }
            return arr;
        }
    }

     另:排序算法的稳定性

    1. 稳定性的定义
    假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,ri=rj,且ri在rj之前,而在排序后的序列中,ri仍在rj之前,则称这种排序算法是稳定的;否则称为不稳定的。

    举个栗子:以{6,2,4,6,1}为例

    a[0] a[1] a[2] a[3] a[4]
    6 2 4 6 1

    有两个6,a[0]和a[3]。排序结果就有两种可能:

    1 2 4 6 6
    原a[4] 原a[1] 原a[2] 原a[0] 原a[3]
    原a[4] 原a[1] 原a[2] 原a[3] 原a[0]

    如果排序结束后,a[0]可以保证一定在a[3]前头,也就是他们原有的顺序不变,那这种排序算法就是稳定的。反之,如果不能保证原有顺序,这种算法就是不稳定的

    2. 常见算法的稳定性
    堆排序、快速排序、希尔排序、直接选择排序不是稳定的排序算法,而基数排序、冒泡排序、直接插入排序、折半插入排序、归并排序是稳定的排序算法。

    3. 稳定性的意义
    1、如果只是简单的进行数字的排序,那么稳定性将毫无意义。
    2、如果排序的内容仅仅是一个复杂对象的某一个数字属性,那么稳定性依旧将毫无意义(所谓的交换操作的开销已经算在算法的开销内了,如果嫌弃这种开销,不如换算法好了?)
    3、如果要排序的内容是一个复杂对象的多个数字属性,但是其原本的初始顺序毫无意义,那么稳定性依旧将毫无意义。
    4、除非要排序的内容是一个复杂对象的多个数字属性,且其原本的初始顺序存在意义,那么我们需要在二次排序的基础上保持原有排序的意义,才需要使用到稳定性的算法。例如要排序的内容是一组原本按照价格高低排序的对象,如今需要按照销量高低排序,使用稳定性算法,可以使得想同销量的对象依旧保持着价格高低的排序展现,只有销量不同的才会重新排序。(当然,如果需求不需要保持初始的排序意义,那么使用稳定性算法依旧将毫无意义)。

    参考:

    https://www.cnblogs.com/songjilong/p/12234856.html

    https://blog.csdn.net/sunxianghuang/article/details/51872360

    https://blog.csdn.net/it_zjyang/article/details/53406764

    https://www.cnblogs.com/hydor/p/3530593.html

    https://blog.csdn.net/chenliguan/article/details/53037482

  • 相关阅读:
    Codeforces 570E
    Codeforces 570D
    Codeforces 1136E
    Codeforces 570
    小白学习sprint boot容易遇到了一些问题
    力扣 234. 回文链表
    力扣198. 打家劫舍
    力扣543. 二叉树的直径
    力扣141.环形链表
    剑指offer1.跳台阶 & 力扣70.爬楼梯
  • 原文地址:https://www.cnblogs.com/zeroingToOne/p/9361191.html
Copyright © 2020-2023  润新知