关于比较排序的一些笔记

基于比较的排序算法

时间复杂度

常数时间的操作

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

时间复杂度为一个算法流程中,常数操作数量的一个指标。通常用 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. 有趣的资料

posted @ 2020-03-24 00:39  带了1个小才艺  阅读(218)  评论(1编辑  收藏  举报