快速回忆十大排序
快速回忆十大排序
Java 中 Arrays.sort()
通过查看 Arrays.sort() 的源码, 可以发现 Arrays.sort() 并不是简单的使用快排,而是根据要排序的长度选择不同的排序。 简单总结:
| 区间长度 | 所使用的排序算法 | 
|---|---|
| 小于 47 | 插入排序 | 
| 大于 47 小于 286 | 快排排序 | 
| 大于 286,数据基本有序 | 归并排序 | 
| 大于 286,数据基本无序 | 快速排序 | 
进行快排时会计算 5 个元素如果有相同元素就进行传统快速排序 ,如果没有相同的就进行双轴快速排序。
插入排序、归并排序、快速排序连源码都在用可见其重要性。
经典十大排序
| 排序算法 | 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 | 
|---|---|---|---|---|---|
| 冒泡排序 | O(n2) | O(n) | O(n2) | O(1) | 稳定 | 
| 选择排序 | O(n2) | O(n2) | O(n2) | O(1) | 不稳定 | 
| 插入排序 | O(n2) | O(n) | O(n2) | O(1) | 稳定 | 
| 希尔排序 | O(n log n) | O(n log2 n) | O(n log2 n) | O(1) | 不稳定 | 
| 归并排序 | O(n log n) | O(n log n) | O(n log n) | O(n) | 稳定 | 
| 快速排序 | O(n log n) | O(n log n) | O(n2) | O(log n) | 不稳定 | 
| 堆排序 | O(n log n) | O(n log n) | O(n log n) | O(1) | 不稳定 | 
| 计数排序 | O(n+k) | O(n+k) | O(n+k) | O(n+k) | 稳定 | 
| 桶数排序 | O(n+k) | O(n2) | O(n) | O(n+k) | 稳定 | 
| 基数排序 | O(n*k) | O(n*k) | O(n*k) | O(n+k) | 稳定 | 
不稳定是指排序过程中会把原本有序区间破化成无序的
排序测试力扣题目
力扣 912.排序数组,地址: https://leetcode.cn/problems/sort-an-array/ 除了冒泡、插入会超时以外,其他排序方式都可以通过。
1. 冒泡排序
这应该是大家接触最早的排序。数就像水里的泡泡一样,泡泡从大到小一个个冒出来。 数组形式: [无序区间] [有序区间] ,在无序区间一直比较并交换把最大的冒到有序区间前。 两层 for 循环,一层循环将一个最大的数放到最后。 动图演示: 
Java 代码
        for(int i = 0; i < nums.length; i++) {
            for(int j = 0; j  < nums.length - 1 - i; j++) {
                if(nums[j] > nums[j + 1]) {
                    //交换
                    int tem = nums[j];
                    nums[j] = nums[j + 1];
                    nums[j  + 1] = tem;
                }
            }
        }2. 选择排序
插入排序总结一句话就是挑最小的放前面,比较的次数多但是交换的次数少 数组形式: [有序区间] [无序区间] ,在无序区间挑一个最小的数跟在有序区间后面。 两层 for 循环,一层循环用来找到最小的数。 动图演示: 
Java 代码:
        int minIndex = 0;
        for(int i = 0; i < nums.length - 1; i++) {
            minIndex = i;
            for(int j = i + 1; j < nums.length; j++) {
               if(nums[j] < nums[minIndex]) {
                   minIndex = j;
               }
           }
           //找到最小数的索引,交换
           int tem = nums[i];
           nums[i] = nums[minIndex];
           nums[minIndex] = tem;
        }3. 插入排序
插入排序总结:拿到一个元素就去找他该在的位置插入进去 数组形式: [有序区间] [无序区间] ,在无序区间拿第一个元素插入到有序区其应该在的位置 从右往左,放第一个比他小的后面,且从右往左遍历过程中把数组右移。
动图演示: 
Java 代码:
        for(int i = 1; i < nums.length; i++) {
            int index = i - 1;
            int tem = nums[i];
            while(index >= 0 && nums[index] > tem) {
                nums[index + 1] = nums[index]; //右移
                index--;
            }
            nums[index + 1] = tem; //把无序部分第一个数插入到有序部分的这里
        }4. 希尔排序
希尔排序是当时最早突破 O(n2)的排序,是插入排序的改进版本。 插入排序主要在于右移是 O(n) 希尔排序的思想是引入一个增量,先两两插入排序,再四四插入排序,最终整体插入排序,这样前面处理完,后面插入移动次数会变少。 每一轮按照事先决定的间隔进行插入排序,间隔依次缩小最后为 1。 动图演示: 
 如果看不懂这个动图,可以先看看这个教程,再回头看这个动图。https://www.runoob.com/data-structures/shell-sort.htmlJava 代码:
        for(int gap = nums.length / 2; gap > 0; gap /= 2) {
            //对每组进行插入排序。 这里和动图不太一样,并不是一组一组进行的,而是各组交叉进行的
            for(int i = gap; i < nums.length; i++) {
                int tem = nums[i];
                int index = i;
                while(index >= gap && nums[index - gap] > tem) {
                    //按分组的位置右移
                    nums[index] = nums[index - gap];
                    index -= gap;
                }
                nums[index] = tem;
            }
        }
        return nums;5. 归并排序
希尔排序是分治法的一个典型应用,将已有序的子序列合并,得到完全有序的序列;
动图演示: 
更直观的图片演示: 
Java 代码:
    int[] tem;
    public int[] sortArray(int[] nums) {
        tem = new int[nums.length];
        mergeSort(nums, 0, nums.length - 1);
        return nums;
    }
    void mergeSort(int[] nums, int l, int r) {
        if(l >= r) return;
        int mid = (l + r) / 2;
        //分
        mergeSort(nums, l, mid);
        mergeSort(nums, mid + 1, r);
        
        //治(归并)
        int i = l, j = mid + 1;
        int index = 0;
        while(i <= mid && j <= r) {
            if(nums[i] <= nums[j]) {
                tem[index++] = nums[i++];
            }else {
                tem[index++] = nums[j++];
            }
        }
        while(i <= mid) tem[index++] = nums[i++];
        while(j <= r) tem[index++] = nums[j++];
        //把归并完成的这部分赋值回给原始数组
        for(int k = 0; k < r - l + 1; k++) {
            nums[k + l] = tem[k];
        }
    }6. 快速排序
通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小, 然后再分别对这两部分记录继续进行排序,以达到整个序列有序。 数组形式: [小数] [基准元素] [大数],在区间中随机挑选一个元素作为基准,小于基准元素的放基准之前,大于基准的放基准之后。 然后再对小数和大数区间做快排
整个流程就是找到基准点应该放置的位置。
动图演示: 
Java 代码:
    public int[] sortArray(int[] nums) {
        quickSort(nums, 0, nums.length - 1);
        return nums;
    }
    public void quickSort(int[] nums, int left, int right) {
        //顺序选取基准值
        if(left >= right) return;
        int first = left, key = nums[first], end = right;
        while (first < end) {
            //先看右边的, 比基准大就不管
            while (first < end && nums[end] >= key) {
                end--;
            }
            //右边end指向的这个值小于分界值, 交换.
            nums[first] = nums[end];
            //然后走左边
            while (first < end && nums[first] <= key) {
                ++first;
            }
            //如果first指针指向的值大于分界值key, 交换
            nums[end] = nums[first];
        }
        // 左右指针相同, 基准值该在这个位置,使得基准值左右两边是比他小和比他大的数
        nums[first] = key;
        //递归, 对左右两边再进行上面操作
        quickSort(nums, left, first - 1);
        quickSort(nums,first + 1, right);
    }如果遇到数组是 9 8 7 6 5 4 3 2 1, 再从左往右顺序选取基准值的话,每次都得移动 n 个, 算法时间复杂度退回到 O(n2)。 所以最好是随机选取基准,或者提前打乱数组,数组越乱,快排越快 随机选取基准值,快排代码:
    public int[] sortArray(int[] nums) {
        quickSort(nums, 0, nums.length - 1);
        return nums;
    }
    public void quickSort(int[] nums, int left, int right) {
        if(left >= right) return;
        //随机选取基准值
        int index = left + (int)(Math.random() * (right - left + 1));
        int key = nums[index];
        int first = left, end = right;
        nums[index] = nums[first];
        nums[first] = key;
        
        while (first < end) {
            //先看右边的, 比基准大就不管
            while (first < end && nums[end] >= key) {
                end--;
            }
            //右边end指向的这个值小于分界值, 交换.
            nums[first] = nums[end];
            //然后走左边
            while (first < end && nums[first] <= key) {
                ++first;
            }
            //如果first指针指向的值大于分界值key, 交换
            nums[end] = nums[first];
        }
        // 左右指针相同, 基准值该在这个位置,使得基准值左右两边是比他小和比他大的数
        nums[first] = key;
        //递归, 对左右两边再进行上面操作
        quickSort(nums, left, first - 1);
        quickSort(nums,first + 1, right);
    }7. 堆排序
利用小顶堆。
待补充
8. 计数排序
记录索引位置数的个数,利用索引本身就有序进行排序。 有一个缺点就是得事先知道数据得范围,范围多大就得开多大数组。 动图演示: 
Java 代码:
    public int[] sortArray(int[] nums) {
        int[] dic = new int[100001];
        for(int i = 0; i < nums.length; i++) {
            dic[nums[i] + 50000]++;
        }
        int index = 0;
        for(int i = 0; i < dic.length; i++) {
            for(int j = 0; j < dic[i]; j++) {
                nums[index++] = i - 50000;
            }
        }
        return nums;
    }9. 桶排序
桶排序是计数排序的升级版, 计数排序是一对一的映射,而桶排序是一对多的映射,把数据分到不同的桶里的元素用其他排序方式排序。 动图演示:
Java 代码:
    public int[] sortArray(int[] nums) {
        int n = nums.length;
        int max = nums[0], min = nums[0];
        for(int i = 0; i < n; i++) {
            if(nums[i] > max) max = nums[i];
            if(nums[i] < min) min = nums[i];
        }
        int diff = max - min;
        //设n个桶
        List<Integer>[] buket = new List[n];
        for(int i = 0; i < n; i++) {
            buket[i] = new ArrayList<>();
        }
        //每个桶的范围,防止为0
        int section = diff / n + 1; 
        for(int i = 0; i < n; i++) {
            int id = (nums[i] - min) / section - 1; // -1防止等于 n
            if(id < 0) id = 0;
            buket[id].add(nums[i]);
        }
        //桶内排序
        for(int i = 0; i < n; i++) {
            Collections.sort(buket[i]);
        }
        //返回排序数组
        int index = 0;
        for(int i = 0; i < n; i++) {
            for(int j = 0; j < buket[i].size(); j++) {
                nums[index++] = buket[i].get(j);
            }
        }
        return nums;
    }10. 基数排序
基数排序是桶排序的扩展, 思想是把整数按每位不同放入不同的桶里,按每位进行比较。 比如 32,34,16, 5 排序, 先按个位数排序 32,34,5,16, 再按十位排序 5,16,32,34 就完事了 【因为之前按个位排序时把 32 放在了 34 前面,所以再进行十位排序时,按顺序来 32 自然在 34 前面】 动图演示: 
Java 代码:
    public int[] sortArray(int[] nums) {
        List<Integer>[] buket = new List[10];
        for(int i = 0; i < 10; i++) buket[i] = new ArrayList<>();
        // 把负数全部转化成正数处理
        int max = nums[0] + 50000;
        //得到最大的数
        for(int t : nums) if(t + 50000 > max) max = t + 50000;
        
        //从个位到最高位
        int len = String.valueOf(max).length(); //最高位位数
        for(int i = 0; i < len; i++) {
            //入桶
            for(int t : nums) {
                buket[((t + 50000) / (int)Math.pow(10, i)) % 10].add(t + 50000);
            }
            //出桶
            int index = 0;
            for(int j = 0; j < 10; j++) {
                for(int k = 0; k < buket[j].size(); k++) {
                    nums[index++] = buket[j].get(k) - 50000;
                }
                buket[j].clear(); //清空桶
            }
        }
        return nums;
    }对链表进行排序
如果你掌握了 插入排序、快速排序、归并排序的精髓,那么试试这三种方法对链表进行排序吧 狂刷三道力扣中等题: 力扣 147. 对链表进行插入排序力扣 148. 排序链表力扣 剑指 Offer II 077. 链表排序
1. 插入排序 O(n2)
插入排序: 【有序】【无序】 无序部分找最小值放有序后头
直接上 Java 代码:
    public ListNode insertionSortList(ListNode head) {
        //由于链表只能从前往后遍历,所以只能从前往后遍历找插入位置
        //没有元素或者只有一个元素直接返回
        if(head == null || head.next == null) return head;
        ListNode dump = new ListNode();
        dump.next = head;
        ListNode lastNode = head;//有序节点的最后一个节点
        ListNode cur = head.next;
        while(cur != null) {
            if(cur.val >= lastNode.val) {
                //直接往后走
                lastNode = lastNode.next;
            }else {
                ListNode pre = dump;
                while(pre.next.val < cur.val) {
                   pre = pre.next;
                }
                //lastNode 不变,但是lastNode下一个数被插入前面了,
                lastNode.next = cur.next;
                cur.next = pre.next;
                pre.next = cur;
            }
            cur = lastNode.next;
        }
        lastNode.next = null;
        return dump.next;
    }2.快速排序 O(n log n)
快速排序: 【小数】【基准元素】【大数】,所有小数移动到基准元素左边,大数移动到基准元素右边
直接上 Java 代码: 对于力扣 148. 排序链表,快排超时了。
    public ListNode sortList(ListNode head) {
        if(head == null || head.next == null) return head;
        ListNode dump = new ListNode();
        dump.next = head;
        return quickSort(dump, null);
    }
    ListNode quickSort(ListNode headPre, ListNode end) {
        if(headPre == end || headPre.next == end || headPre.next.next == end) return headPre;
        //将小于基准值的节点存在这个临时链表中
        ListNode temDump = new ListNode();
        ListNode tp = temDump;
        //key 为基准节点,基准节点左边是比他小的,右边是比他大的
        ListNode key = headPre.next, lp = headPre;
        while(lp.next != end) {
            if(lp.next.val < key.val) {
            //节点值小于基准节点,就把这个节点放到左边
                tp.next = lp.next;
                tp = tp.next;
                lp.next = lp.next.next;
            }else {
                lp = lp.next;
            }
        }
        //合并大小两个链表
        tp.next = key;
        //整个链表用headPre连回来
        headPre.next = temDump.next; // 链表插回原链表(如果不做这一步,处理右半部分的时候就断了)
        quickSort(headPre, key);
        quickSort(key, end);
        return headPre.next;
    }2.归并排序 O(n log n)
链表排序最好就是用归并排序, 链表的归并排序不需要额外空间 归并,分治
归并排序需要不断的把链表分为两段,分为两段的方法可以用快慢指针,快指针溢出走两步,慢指针一次走一步,快指针走到头慢指针指向中间节点。
直接上 Java 代码:
class Solution {
    public ListNode sortList(ListNode head) {
        if(head == null || head.next == null) return head;
        return mergeSort(head);
    }
    ListNode mergeSort(ListNode head) {
        if(head.next == null) return head;
        // 分
        //找中间节点
        ListNode fast = head, slow = head, pre = null;//pre 是slow节点的前一个节点
        while(fast != null && fast.next != null) {
            fast = fast.next.next;
            pre = slow;
            slow = slow.next;
        }
        pre.next = null; //断链, 现在head只有一半链表, slow 是另一半链表
        ListNode left = mergeSort(head); // 继续把head这一半进行分半
        ListNode right = mergeSort(slow); //继续把slow这一半进行分半
        //治
        //left、 right都是从最小(一个节点)回溯到最长(一半)
        //合并left、right
        ListNode dump = new ListNode();
        ListNode cur = dump;
        while(left != null && right != null) {
            if(left.val <= right.val) {
                cur.next = left;
                left = left.next;
                cur = cur.next;
            }else {
                cur.next = right;
                right = right.next;
                cur = cur.next;
            }
        }
        if(left != null) cur.next = left;
        if(right != null) cur.next = right;
        return dump.next;
    }
}