• 关于比较排序的一些笔记


    基于比较的排序算法

    时间复杂度

    常数时间的操作

    一个操作如果和样本的数据量没有关系,每次都是固定时间内完成的操作,叫做常数操作。

    时间复杂度为一个算法流程中,常数操作数量的一个指标。通常用 O(读 big O)来表示。

    在表达式中,只要高阶项,不要低阶项,也不要高阶项的系数,剩下的部分如果为f(N),那么时间复杂度为0(f(N))。

    评价一个算法流程的好坏,先看时间复杂度的指标,然后再分析不同数据样本下的实际运行时间,也就是“常数项时间”。

    O(N²) 时间复杂度的排序

    1. 选择排序

    每次找到 i 往后最小的元素,跟 i 交换

    // big O(N²)
    public static void selectionSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        for (int i = 0; i < arr.length - 1; i++) {
            int minIndex = i;
            for (int j = i + 1; j < arr.length; j++) {
                minIndex = arr[j] < arr[minIndex] ? j : minIndex;
            }
            swap(arr, i, minIndex);
        }
    }
    

    2. 冒泡排序

    每次让最小的浮上来

    // big O(N²)
    public static void bubbleSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        for (int e = arr.length - 1; e > 0; e--) {  // 0 ~ e
            for (int i = 0; i < e; i++) {
                if (arr[i] > arr[i + 1]) {
                    swap(arr, i, i + 1);
                }
            }
        }
    }
    

    3. 插入排序

    类似扑克牌排序emmm

    // 最好O(N),最差O(N²),所以时间复杂度为O(N²),额外空间复杂度O(1)
    public static void insertionSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        for (int i = 1; i < arr.length; i++) { 
            for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
                swap(arr, j, j + 1);
            }
        }
    }
    

    递归行为的时间复杂度

    public class GetMax {
        public static int getMax(int[] arr) {
            if (arr == null || arr.length == 0) {
                //
            }
            return process(arr, 0, arr.length - 1);
        }
    
        // arr[L..R]范围上求最大值
        private static int process(int[] arr, int L, int R) {
            if (L == R) { // arr[L..R]范围上只有一个数,直接返回
                return arr[L];
            }
            int mid = L + ((R - L) >> 1); // 中点
            int leftMax = process(arr, L, mid);
            int rightMax = process(arr, mid + 1, R);
            return Math.max(leftMax, rightMax);
        }
    }
    
    • 上面代码的时间复杂度(L到R上N个数):
      • T(N) = 2T(N/2) + O(1)
        • 按照master公式:a=2, b=2, d=0 故而 时间复杂度为O(N)

    • master公式只能解决子问题数据规模一样的递归

    O(N*logN)时间复杂度的排序

    为什么求中点不推荐写 **mid = ( L + R ) / 2 **?

    • 防止溢出,如果下标非常大,有可能溢出。
    • mid = L + ( R - L ) / 2
    • mid = L + (( R - L ) >> 1 )

    1. 归并排序

    整体就是简单的递归,左边排序,右边排序,再整体排序。

    • 时间复杂度O(N*logN),额外空间复杂度O(N)
    public class MergeSort {
        public static void mergeSort(int[] arr) {
            if (arr == null || arr.length < 2) {
                return;
            }
            process(arr, 0, arr.length - 1);
        }
    
        public static void process(int[] arr, int L, int R) {
            if (L == R) {
                return;
            }
            int mid = L + ((R - L) >> 1);
            process(arr, L, mid);
            process(arr, mid + 1, R);
            merge(arr, L, mid, R);
        }
    
        public static void merge(int[] arr, int L, int M, int R) {
            int[] help = new int[R - L + 1];
            int i = 0;
            int p1 = L;
            int p2 = M + 1;
            while (p1 <= M && p2 <= R) {
                help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
            }
            while (p1 <= M) {
                help[i++] = arr[p1++];
            }
            while (p2 <= R) {
                help[i++] = arr[p2++];
            }
            for (i = 0; i < help.length; i++) {
                arr[L + i] = help[i];
            }
        }
    }
    

    2. 快速排序

    荷兰国旗问题

    • 不改进的快速排序
      • 把数组范围中的最后一个数作为划分值,然后数组分成三个部分:
        • 左侧 < 划分值
        • 中间 == 划分值
        • 右侧 > 划分值
      • 划分值越靠近两侧,复杂度越高,划分值越靠近中间,复杂度越低
      • 所以不改进的快速排序时间复杂度为O(N^2)
    • 改进后的快速排序(随即快速排序
      • 在数组范围中,等概率随机选一个数作为划分值
      • 时间复杂度是按照最差情况来计算的,但是如果引入了概率,那么最差情况将会变成概率事件。
        • 每个位置被选中的概率都是均等的。 1/N
        • 复杂度的长期期望,收敛于O(N*logN)
        • 额外空间复杂度:最优 O(logN) , 最差O(N)
    public class QuickSort {
        public static void quickSort(int[] arr) {
            if (arr == null || arr.length < 2) {
                return;
            }
            quickSort(arr, 0, arr.length - 1);
        }
    
        // arr[L..R]排序
        private static void quickSort(int[] arr, int L, int R) {
            if (L < R) {
                swap(arr, L + (int) (Math.random() * (R - L + 1)), R); // 取一个随机位置的数,跟最后一个位置做交换(这条语句将会改进快排变为概率事件)
                int[] p = partition(arr, L, R);
                quickSort(arr, L, p[0] - 1); // < 区
                quickSort(arr, p[1] + 1, R); // > 区
            }
        }
    
        // 这是一个处理arr[L..R]的函数
        // 默认以arr[R]做划分,arr[R]->p, <p ==p >p
        // 返回等于区域(左边界,右边界),所以返回一个长度为2的数组res,res[0],res[1]
        public static int[] partition(int[] arr, int L, int R) {
            int less = L - 1; // < 区右边界
            int more = R; // > 区左边界
            while (L < more) { // L 表示当前数的位置 arr[R] -> 划分值
                if (arr[L] < arr[R]) {
                    swap(arr, ++less, L++);
                } else if (arr[L] > arr[R]) {
                    swap(arr, --more, L);
                } else {
                    L++;
                }
            }
            swap(arr, more, R);
            return new int[] { less + 1, more };
        }
    
        public static void swap(int[] arr, int i, int j) {
            if (i == j) {
                return;
            }
            arr[i] = arr[i] ^ arr[j];
            arr[j] = arr[i] ^ arr[j];
            arr[i] = arr[i] ^ arr[j];
        }
    }
    

    3. 堆排序

    • 堆结构,就是用数组实现的完全二叉树
      • 什么是完全二叉树?
        • 完全二叉树:满树 或者 处在逐渐变满的路上
    • 大根堆:每颗子树的最大值在顶部
    • 小根堆:每颗子树的最小值在顶部
    • heapInsert 和 heapify 操作
    • 对于任意一个 i :
      • 其左孩子 = 2i + 1
      • 其有孩子 = 2i + 2
      • 父节点: ( i - 1 ) / 2

    排序

    • 先让整个数组都变成大根堆结构,建立堆的过程:
      • 从上到下的方法,时间复杂度为O(N*logN)
      • 从下到上的方法,时间复杂度为O(N)
    • 把堆的最大值和堆末尾的值交换,然后减少堆的大小之后,再去调整堆,一直周而复始,时间复杂度为O(n*logN)
    • 堆大小减小成0之后,排序完成
    • 时间复杂度:O(N*logN) , 额外空间复杂度:O(1)
    public class HeapSort {
        public static void heapSort(int[] arr) {
            if (arr == null || arr.length < 2) {
                return;
            }
            // 数组变大根堆(1): O(N*logN)
            // for (int i = 0; i < arr.length; i++) { // O(N)
            // heapInsert(arr, i); // O(logN)
            // }
            // 数组变大根堆(2): O(N)
            // T(N) = N/2 + (N/4)*2 + (N/8)*3... : 2T(N)-T(N) = T(N) = N + N/2 + N/4 ...
            // 最终收敛到O(N)
            for (int i = arr.length - 1; i >= 0; i--) {
                heapify(arr, i, arr.length);
            }
    
            int heapSize = arr.length;
            while (heapSize > 0) { // O(N)
                swap(arr, 0, --heapSize); // O(logN)
                heapify(arr, 0, heapSize); // O(1)
            }
        }
    
        // 某个数现在处于index位置,往上继续移动
        public static void heapInsert(int[] arr, int index) {
            while (arr[index] > arr[(index - 1) / 2]) {
                swap(arr, index, (index - 1) / 2);
                index = (index - 1) / 2;
            }
        }
    
        // 某个数再index位置,能否往下移动
        public static void heapify(int[] arr, int index, int heapSize) {
            int left = index * 2 + 1; // 左孩子的下标
            while (left < heapSize) { // 下方还有孩子的时候
                // 两个孩子中,谁的值大,把下标给largest
                int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
                // 父节点和较大的孩子节点之间,谁的值大,就把下标给largets
                largest = arr[largest] > arr[index] ? largest : index;
                if (largest == index) {
                    break;
                }
                swap(arr, largest, index);
                index = largest;
                left = index * 2 + 1;
            }
        }
    
        public static void swap(int[] arr, int i, int j) {
            if (i == j) {
                return;
            }
            arr[i] = arr[i] ^ arr[j];
            arr[j] = arr[i] ^ arr[j];
            arr[i] = arr[i] ^ arr[j];
        }
    }
    
    • Java 默认系统实现 — 堆 : PriorityQueue

    • 为什么不能从上往下 heapify 呢?

      • 例如:1 | 0 0 | 9 9 9 9 这种树。。。而从下往上就能解决这种问题

    二分查找

    // 时间复杂度为O(logN)
    public static boolean binarySearch(int[] arr, int num) {
        if (arr == null | arr.length == 0) {
            return false;
        }
        int L = 0;
        int R = arr.length - 1;
        while (L < R) {
            int mid = L + ((R - L) >> 1);
            if (arr[mid] == num) {
                return true;
            } else if (arr[mid] > num) {
                R = mid - 1;
            } else {
                L = mid + 1;
            }
        }
        return arr[L] == num;
    }
    
    • 当然,也可以写成递归版本的。
    public static boolean binarySearch(int[] arr, int num, int L, int R) {
            if (arr == null || arr.length == 0 || R < L) {
                return false;
            }
            int mid = L + ((R - L) >> 1);
            if (arr[mid] == num) {
                return true;
            } else if (arr[mid] > num) {
                return binarySearch(arr, num, L, mid - 1);
            } else {
                return binarySearch(arr, num, mid + 1, R);
            }
        }
    

    异或(exclusive OR):无进位相加

    • 0 ^ N = N

    • N ^ N = 0










    练习题目

    二分查找相关题目

    1. 有序数组中,找大于等于某个数的最左侧的位置

    • 解法:
    // 在arr上,找满足>=value的最左位置
    public static int nearestIndex(int[] arr, int value) {
        int L = 0;
        int R = arr.length - 1;
        int index = -1;
        while (L < R) {
            int mid = L + ((R - L) >> 1);
            if (arr[mid] >= num) {
                index = mid;
                R = mid - 1;
            } else {
                L = mid + 1;
            }
        }
        return index;
    }
    

    2. 局部最小值问题

    • 解法:
    public static int getMin(int[] arr) {
        return process(arr, 0, arr.length - 1);
    }
    public static int process(int[] arr, int L, int R) {
        if (L == R) {
            return arr[L];
        }
        int mid = L + ((R - L) >> 1);
        int leftMin = process(arr, L, mid);
        int rightMin = process(arr, mid + 1, R);
        return Math.min(leftMin, rightMin);
    }
    



    异或相关题目

    1. 找到奇数次的数

    • 一个数组中有一种数出现了奇数次,其他数都出现了偶次数,怎么找到这个数?

    • 解:

    public static void printOddTimesNum1 (int[] arr){
        int eO = 0;	// 任何一个数,异或0,等于自己
        for(int i:arr){
            eO ^ i;
        }
        System.out.println(eO);
    }
    
    • 如果有两种数出现了奇数次,并把它们找出来?

    • 解:

      1. a≠ b => eor = a ^ b ≠ 0,那么 eor 一定有个位置是1。

      2. 假设eor第8位是1,那么整个数组可以被分为两块区域,一个是第8位是1的数,另一个是第8位是0的数。一定互斥,a 和 b必然分开。

      3. 这时候异或所有的第8位是1/0的数字,就会得到a或者b的其中一个

      4. 再用拿到的数字异或eor,就会得到另外一个(当然也可以两次异或所有,只是这个更快)

    public static void printOddTimesNum2(int[] arr) {
        int eor = 0;
        for (int i : arr) {
            eor ^= i;
        }
        // eor = a ^ b != 0,必然有一个位置上是1
        int rightOne = eor & (~eor + 1); // 提取最右侧的1
        int anotherOne = 0;
        for (int i : arr) {
            if ((i & rightOne) != 0) {
            // if ((i & rightOne) == 0) {
                anotherOne ^= i;
            }
        }
        System.out.println(anotherOne + " " + (eor ^ anotherOne));
    }
    
    • 如何获得最右侧的 1 呢?
    二进制 意义
    a 0 1 1 0 1 0 0 0
    a - 1 0 1 1 0 0 1 1 1
    a & a - 1 0 1 1 0 0 0 0 0
    a ^ ( a & a-1 ) 0 0 0 0 1 0 0 0 方法1:提取最右侧的1
    或者可以
    ~a 1 0 0 1 0 1 1 1
    ~a + 1 1 0 0 1 1 0 0 0 代表只有最右侧的1和它右边被保留了,左边全相反
    a & ( ~a + 1 ) 0 0 0 0 1 0 0 0 方法2:提取最右侧的1

    归并排序相关题目

    1. 小和问题和逆序对问题

    小和问题:在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和。

    • 例子:[1,3,4,2,5]
      • 1 左边比 1 小的数,没有;
      • 3 左边比 3 小的数,1;
      • 4 左边比 4 小的数,1、3;
      • 2 左边比 2 小的数,1;
      • 5 左边比 5 小的数,1、3、4、2;
      • 所以小和为 1+1+3+1+1+3+4+2=16

    逆序对问题:在一个数组中,左边的数如果比右边的数大,则这两个数构成一个逆序对,打印出所有逆序对。

    public class SmallSum {
        // 左边比当前数小的数累加,等同于(右边比当前大的个数*当前数字)的累加
        public static int smallSum(int[] arr) {
            if (arr == null || arr.length < 2) {
                return 0;
            }
            return process(arr, 0, arr.length - 1);
        }
    
        // arr[L..R]既要排好序,也要求小和
        public static int process(int[] arr, int l, int r) {
            if (l == r) {
                return 0;
            }
            int mid = l + ((r - l) >> 1);
            return process(arr, l, mid) + process(arr, mid + 1, r) + merge(arr, l, mid, r);
        }
    
        private static int merge(int[] arr, int l, int mid, int r) {
            int[] help = new int[r - l + 1];
            int i = 0;
            int p1 = l;
            int p2 = mid + 1;
            int res = 0;
            while (p1 <= mid && p2 <= r) {
                res += arr[p1] < arr[p2] ? (r - p2 + 1) * arr[p1] : 0;	
                // 排序的同时,算出右边有多少个比自己大的,求和
                help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
            }
            while (p1 <= mid) {
                help[i++] = arr[p1++];
            }
            while (p2 <= r) {
                help[i++] = arr[p2++];
            }
            for (i = 0; i < help.length; i++) {
                arr[l + i] = help[i];
            }
            return res;
        }
    }
    

    快速排序相关题目

    荷兰国旗问题

    1. 问题一

    给定一个数组arr,和一个数num,请把小于等于num的数放在数组的左边,大于num的数放在数组的右边。要求额外空间复杂度0(1),时间复杂度0(N)

    2. 问题二

    给定一个数组arr,和一个数num,请把小于num的数放在数组的左边,等于num的数放在数组的中间,大于num的数放在数组的右边。要求额外空间复杂度0(1),时间复杂度0 (N)

    public class NetherlandsFlag {
        // 荷兰国旗问题
        public static int[] partition(int[] arr, int l, int r, int p) {
            int less = l - 1; // < 区的右边界
            int more = r + 1; // > 区的左边界
            while (l < more) { // L 是当前数的下标
                if (arr[l] < p) {
                    swap(arr, ++less, l++);
                } else if (arr[l] > p) {
                    swap(arr, --more, l);
                } else {
                    l++;
                }
            }
            return new int[] { less + 1, more - 1 };
        }
    
        private static void swap(int[] arr, int i, int j) {
            int tmp = arr[i];
            arr[i] = arr[j];
            arr[j] = tmp;
        }
    }
    










    总结

    1. 比较器

    • 比较器的实质是重载比较运算符
    • 可以很好的应用在特殊标准的排序上
    • 可以很好的应用在根据特殊标准排序的结构上

    2. 基于比较的排序算法的总结

    1. 不具备稳定性的排序:
    • 选择排序
    • 快速排序
    • 堆排序
    2. 具备稳定性的而排序:
    • 冒泡排序
    • 插入排序
    • 归并排序
    • 一切桶排序思想下的排序下(但是不是**基于比较的排序)

    3. 有趣的资料

  • 相关阅读:
    收集最好的Mac软件和使用方法
    为什么使用Binder而不是其他IPC机制
    Android什么时候进行View中Background的加载
    Android属性动画源代码解析(超详细)
    JMockit工具总结
    路由器和交换机的区别
    6种 @Transactional 注解失效场景
    20个使用 Java CompletableFuture的例子
    书写高质量SQL的30条建议
    Netty之什么是 TCP 拆、粘包?如何解决?
  • 原文地址:https://www.cnblogs.com/pipemm/p/12556371.html
Copyright © 2020-2023  润新知