跟着左神学算法:P4 详解桶排序以及排序内容大总结

182 阅读2分钟

P4:详解桶排序以及排序内容大总结

完全二叉树

定义

如果一个树是满二叉树,那么它是完全二叉树;如果它不是满二叉树,但是最后一层的叶子节点是从左往右依次排列的,那么它也是完全二叉树。

存储

可以用一个数组存储完全二叉树(等价于它的先根遍历)

数组下标和完全二叉树节点位置关系

节点在数组中下标为 i,它的左子节点在数组中下标为 2i+1, 右子节点在数组中下标为 2i+2,父节点在数组中下标为 (i-1)/2

大根堆(大顶堆)

完全二叉树中父节点比左右子树中全部节点都大,成为大根堆
把一个数组构建成大根堆

   public int[] buildHeap(int[] array) {
        if (array == null || array.length < 2) {
            return array;
        }
        int[] heap = new int[array.length];
        for (int heapSize = 0; heapSize < heap.length; heapSize++) {
            //先把数放在堆中
            heap[heapSize] = array[heapSize];
            int i = heapSize;
            //把新插入的数据和父节点比较,大于父节点就交换两个节点的值(heapInsert)
            while (heap[(i - 1) / 2] < heap[i]) {
                int tmp = heap[(i - 1) / 2];
                heap[(i - 1) / 2] = heap[i];
                heap[i] = tmp;
                i = (i - 1) / 2;
            }
        }
        return heap;
    }

heapInsert过程

//注意这里 heapSize 有变化,调用者需要修改
public void heapInsert(int []heap, int heapSize, int num) {
    heap[heapSize] = num;
    int i = heapSize;
    while (heap[(i - 1) / 2] < heap[i]) {
        swap(heap, (i - 1) / 2, i);
        i = (i - 1) / 2;
    }
}

private void swap(int[] arr, int i, int j) {
    int tmp = arr[i];
    arr[i] = arr[j];
    arr[j] = tmp;
}

heapify过程

//注意这里 heapSize 有变化,调用者需要修改
public int heapify(int[] heap, int heapSize) {
    int res = heap[0];
    int i = 0;
    int left = 2 * i + 1;
    //先把最后一个节点的数复制到第一个节点(根)
    heap[0] = heap[heapSize - 1];
    //如果当前节点比左右子节点的最大值小,把当前节点和最大的子节点交换
    while(left < heapSize) {
        int maxChildIndex = (left + 1 < heapSize) && (heap[left + 1] > heap[left]) ? left + 1 : left;
        if (heap[maxChildIndex] > heap[i]) {
            swap(heap, maxChildIndex, i);
            i = maxChildIndex;
            left = 2 * i + 1;
        } else {
            break;
        }
    }
    return res;
}

直接修改堆中的某个值,怎么调整堆
heapInsert + heapify

heapInsert 和 heapify 时间复杂度 O(logN)

小根堆(小顶堆)

完全二叉树中父节点比左右子树中全部节点都小,成为小根堆

堆排序

public void heapSort(int[]arr) {
    if(arr == null || arr.length < 2) {
        return;
    }
    int heapSize = 0;
    //先构造大根堆
    for(heapSize = 0; heapSize < arr.length; heapSize ++) {
        int i = heapSize;
        while(arr[i] > arr[(i - 1) / 2]) {
            swap(arr, i, (i - 1) / 2);
            i = (i - 1) / 2;
        }
    }
    //把最大值和堆最后一个节点交换,之后heapify
    //其实就是每次找出最大值,放在最后
    while(heapSize > 0) {
        arr[heapSize - 1] = heapify(arr, heapSize);
        heapSize--;
    }
    return arr;
}

堆排序算法复杂度 O(NlogN)

Java 中 PriorityQueue 底层实现是用堆,扩容容量翻倍

堆排序扩展题目

已知一个几乎有序的数组(几乎有序是指如果把数组排好顺序的话,每个元素移动的距离可以不超过 k 且 k 相对于数组来说比较小),请选择一个合适的排序算法针对这个数据进行排序

public void sortDistanceLessK(int[] arr, int k) {
    PriorityQueue<Integer> heap = new PriorityQueue<>();
    int index = 0;
    for (index = 0; index <= Math.min(arr.length, k + 1); index++) {
        heap.add(arr[index]);
    }
    int i = 0;
    for (; index < arr.length; i++, index++) {
        heap.add(arr[index]);
        arr[i] = heap.poll();
    }
    while (!heap.isEmpty()) {
        arr[i++] = heap.poll();
    }
}

比较器的使用

实际上就是 Comparator 接口

桶排序

需要排序的数据范围有限,可以把数据数量映射到数组上,进行桶排序
例如:员工按照年龄排序

基数排序

先找出最大的数有多少位,把其他数字补齐相应的位数。然后准备 0 - 9 这 10 个桶,按照最后一位落在相应的桶里,然后按照从 0 - 9 的顺序出桶,再按照倒数第二位进桶... 循环下去,当按照第一位进桶出桶之后,就完成了排序


public void radixSort(int[] arr) {
    if(arr == null || arr.length < 2) {
        return arr;
    }
    radixSort(arr, 0, arr.length - 1, maxDigit(arr));
}

public void radixSort(int[] arr, int l, int r, int digit) {
    final int radix = 10;
    int i = 0;
    int j = 0;

    int[] help = new int[r - l + 1];
    for(int d = 1; d <= digit; d++) {
        // 10 个空间
        // count[0] 当前位(d位)是0的数字有多少个
        // count[1] 当前位(d位)是0、1的数字有多少个
        // count[2] 当前位(d位)是0、1、2的数字有多少个
        int[] count = new int[radix];
        // d = 1 这里得到所有数字的个位数的个数(有多少个0,多少个1,多少个2...)
        for(i = l; i <= r; i++) {
            j = getDigit(arr[i], d);
            count[j]++;
        }
        // 如果有 3 个 0,2 个 1,1 个 2;那么count[2] = 6,意思是个位数 <= 2 的有6个数
        for(int i = 1; i < radix; i++) {
            count[i] = count[i] + count[i - 1];
        }
        // 数组从右往左遍历,先拿最后进去的数,最后进去的数放在 help 数组后边
        // 
        for(int i = r; i >= l; i--) {
            j = getDigit(arr[i], d);
            help[count[j] - 1] = arr[i];
            count[j]--;
        }
        // 把 help 数组的数据放回去
        for(int i = l; i <= r; i++){
            arr[i] = help[i - l];
        }
    }
}

private int maxDigit(int arr[]) {
    int max = 0;
    for(int i = 0; i < arr.length; i++) {
        max = Math.max(max, arr[i]);
    }
    int res = 0;
    while(max != 0) {
        res++;
        max /= 10;
    }
    return res;
}

private int getDigit(int num, int d) {
    for(int i = 1; i < d; i++) {
        num /= 10
    }
    return num % 10;
}

基数排序时间复杂度 O(dN)

排序稳定性

定义

相同元素能保证排序之后保持原来的相对顺序的排序算法是稳定排序算法

应用情况

多次排序的情况下,需要使用到稳定排序。
例如在淘宝上先按照销量排序,再按照好评排序,这就需要用稳定排序算法

稳定的排序算法

  • 冒泡排序
    冒泡排序遇到相等的数据不交换
  • 插入排序
  • 归并排序
    merge的时候相等的时候先拷贝左边的结果
  • 基数排序

不稳定的排序算法

  • 堆排序
  • 快速排序
  • 选择排序

排序算法总结

时间复杂度空间复杂度稳定性
选择排序O(N2)O(1)
冒泡排序O(N2)O(1)
插入排序O(N2)O(1)
归并排序O(NlogN)O(N)
快速排序O(NlogN)O(logN)
堆排序O(NlogN)O(1)

选择排序算法时选择快排,快排在常数优化上有优势,实际应用上会快一些

综合排序,在大范围的时候进行快排递归,递归到范围小的时候使用插入