面试_算法_堆排序

159 阅读6分钟

预备知识

堆排序

堆排序是一种利用 这种数据结构完成的 选择排序 , 它的最坏,最好,平均时间复杂度均为O(nlogn),也是不稳定排序。


堆是一种 完全二叉树,它分为两种:最大堆和最小堆,两者的差别在于节点的排序方式。

大顶堆:每个结点的值都大于或等于其左右孩子结点的值。(左右孩子的顺序不区分)

小顶堆:每个结点的值都小于或等于其左右孩子结点的值。(左右孩子的顺序不区分)

同时,我们对堆中的结点按层进行编号,将这种逻辑结构映射到数组中就是下面这个样子

该数组从逻辑上讲就是一个堆结构,我们用简单的公式来描述一下堆的定义就是:

大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]

小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]




堆排序基本思想及步骤

堆排序的基本思想:

将原始序列构调整一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与数组末尾元素交换,此时最大元素"沉"到数组末端。然后将剩余n-1个元素重新调整成一个堆,这样会得到n-1个元素的最大值,将其与数组倒数第二位元素交换,此时次大值也确定了位置。如此反复执行,便能得到一个有序序列。



调整步骤

建堆和调整堆中都有调整动作,他们步骤是一致的。还是以大顶堆为例,如果当前节点小于它的左右孩子之一,则与这个孩子交换位置;如果比任何一个孩子都小。则与较大孩子交换。因交换动作导致子树顺序发生变化的,按此顺序递归地往下调整。



实例

步骤一:构造初始堆。将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。

1.假设给定无序序列结构如下

2.此时我们从最后一个非叶子结点开始(叶结点自然不用调整,第一个非叶子结点 arr.length/2-1=5/2-1=1,也就是下面的6结点),从左至右,从下至上进行调整。

4.找到第二个非叶节点4,由于[4,9,8]中9元素最大,4和9交换。

这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中6最大,交换4和6。

此时,我们就将一个无需序列构造成了一个大顶堆。


步骤二 :调整堆。将堆顶元素与末尾元素进行交换,使末尾元素最大(被换到末尾的元素不再参与后续的调整动作)。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。

a.将堆顶元素9和末尾元素4进行交换

b.重新调整结构,使其继续满足堆定义

c.再将堆顶元素8与末尾元素5进行交换,得到第二大元素8.

后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序



复杂度分析

  • 时间复杂度

    建堆时间为O(n)O(n),之后有n1n-1次向下调整操作,每次调整的时间复杂度为O(h)O(h)。故在最好、最坏、平均情况下时间复杂度都为O(nlog2n)O(nlog_2n)

  • 空间复杂度:O(1)O(1)



算法特性

  • 跳跃交换,导致不稳定

  • 从后往前调整交换,导致适用于顺序结构,但不适用于链式结构

  • 任何情况下时间复杂度都与归并排序相同,但空间复杂度为O(1)O(1),这是它相对于归并排序的最大优点。

    在最坏情况下时间复杂度也是O(nlog2n)O(nlog_2n),这是它相对于快速排序的最大优点。同时空间复杂度也有领先。

  • 堆排序适合的场景是元素很多的情况,典型的例子是从10000个元素中选出前10个最小的,这种情况用堆排序最好。如果元素较少,则不提倡使用堆排序,因为初始建堆所需的比较次数较多。



代码实现

import java.util.Arrays;


public class A {

    static int[] arr = {1, 5, 3, 2, -7, 8, 0, 9, 4, 6};
    static int len = arr.length;

    public static void swap(int[] arr, int a, int b) {
        int temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }

    public static void heapSort() {
        // 建初堆
        for (int i = len / 2 - 1; i >= 0; i--) {
            adjustHeap(arr, i, arr.length);   // i初始为len/2-1,表示它是最后一个非终端结点。从这里开始i--,一直往前分析
        }
        // 调整堆
        for (int j = len - 1; j > 0; j--) {
            swap(arr, 0, j); // 元素交换,把大顶堆的根元素,放到数组的最后
            adjustHeap(arr, 0, j); // 元素交换之后,毫无疑问,最后一个元素无需再考虑排序问题了。调整剩下的元素从新变成堆
        }
    }

    // 非递归调整方法
    public static void adjustHeap(int[] array, int i, int length) {
        // 先把当前元素取出来,因为当前元素可能要一直移动
        int temp = array[i];
        // 传进来当前结点编号i, k = 2*i+1 和 k+1 分别是他的左右孩子
        // for循环的目的是:如果当前结点已经很靠上了,如果调整一下可能会影响下面的子树,所以要持续地往下检查是否满足要求
        for (int k = 2 * i + 1; k < length; k = 2 * k + 1) { // 结点从i开始一个一个处理,直到
            // 这个if语句的目的是判断左右孩子节点哪个大。k是左孩子,k+1是右孩子。如果右孩子大,就k++,代表k指向右子结点
            if (k + 1 < length && array[k] < array[k + 1]) {
                k++;
            }
            // 判断孩子节点中大较大者是否比父节点值要大。如果是,就交换,然后进入下一个for循环,检查子树是否还满足要求;如果不是,就break跳出函数,重新传入前一个非终端节点
            if (array[k] > temp) {   //如果子节点大于父节点,将子节点值赋给父节点(不用进行交换)
                swap(array, i, k);  // 交换
                i = k;   // 为了配合下一个for循环堆其子树的检查,这里要更新一下当前位置
            } else {
                break;
            }
        }
        //arr[i] = temp; // 这一句加不加都可以。
    }

//
//    // 递归调整方法
//    public static void adjustHeap(int[] array, int i, int length) {
//        int left = 2 * i + 1;     // 左孩子
//        int right = 2 * i + 2;    // 右孩子
//        int maxIndex = i;
//        if (left < length && array[left] > array[maxIndex]) maxIndex = left;
//        if (right < length && array[right] > array[maxIndex]) maxIndex = right;
//        if (maxIndex != i) {
//            swap(array, i, maxIndex);
//            adjustHeap(array, maxIndex, length);
//        }
//    }


    public static void main(String[] args) {
        heapSort();
        System.out.println(Arrays.toString(arr));
    }
}

如果想升序,就用最大堆,即上面的代码;如果想降序,就把 for循环 里两个 if语句 的比较符号都反过来,> 变成 << 变成 >



重要的应用:求最小的k个数

如果想利用最小堆来计算前k小的数,改完上面提到的比较符号后,替换掉 heapSort函数 代码为下面的,就够了。

 public static ArrayList heapSort() {
        ArrayList<Integer> list  = new ArrayList<>();
     	// 建初堆
        for (int i = len / 2 - 1; i >= 0; i--) { // 这里也可以改成i>=len/2-1-k,但有时候i会变成负数,越界
            adjustHeap(arr, i, arr.length);   
        }
        // 调整堆
        for (int j = len - 1; j > len -1 - k; j--) {
            list.add(arr[0]);  // 依次装填堆顶的最小值
            swap(arr, 0, j); 
            adjustHeap(arr, 0, j); 
        }
        return list;
    }

另外,也可以使用最大堆来巧妙地求求最小的k个数。性能更好。代码如下:

import java.util.ArrayList;
public class Solution {
    public static void swap(int[] arr, int a, int b) {
        int temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }

    // 原最大堆的调整代码,不动
    public static void adjustHeap(int[] array, int i, int length) {
        int temp = array[i];
        for (int k = 2 * i + 1; k < length; k = 2 * k + 1) {
            if (k + 1 < length && array[k] < array[k + 1]) {
                k++;
            }
            if (array[k] > temp) {
                swap(array, i, k);
                i = k;
            } else {
                break;
            }
        }
    }

public static ArrayList<Integer> GetLeastNumbers_Solution(int[] input, int k) {
        ArrayList<Integer> list = new ArrayList<>();
        int[] arr = new int[k];  //用于放最小的k个数
        for (int i = 0; i < k; i++)
            arr[i] = input[i];//先放入前k个数
        // 建立前k个无序元素的初堆
        int len = arr.length;
        for (int i = len / 2 - 1; i >= 0; i--) {
            // i初始为len/2-1,表示它是最后一个非终端结点。从这里开始i--,一直往前分析
            adjustHeap(arr, i, len);
        }
        // 动态调整堆
        for (int i = k; i < input.length; i++) {
            // 此时arr[0]是前k个无序元素的最大者。然后比较原无序数组里前k个元素后面是否还有比这个最大值小的,如果有,就替换掉,再重新调整堆
            if (input[i] < arr[0]) { //存在更小的数字时
                arr[0] = input[i];  // 替换
                //重新调整最大堆。因为数组的末端是已经求得的最小值,不能再动了,所以每次要k-1
                adjustHeap(arr, 0, k - 1);
            }
        }
        // 最后arr中剩下的就是原无序数组里前k小的值
        for (int i : arr)
            list.add(i);
        return list;
    }

    public static void main(String[] args) {
        int[] arr = {4, 5, 1, 6, 2, 7, 3, 8};
        System.out.println(GetLeastNumbers_Solution(arr, 4));
    }
}




www.cnblogs.com/chengxiao/p…

www.nowcoder.com/practice/6a…