堆排序

堆排序

由于堆排序是一种非常漂亮的排序,是算法与数据结构结合的典例,所以单独拿出来说说哈。

什么是堆


以最大值堆(大根堆)为例。大根堆是一棵完全二叉树,且二叉树上每一个节点R有:R的关键码大于等于其两个直接孩子的关键码。简而言之堆是一种局部有序的完全二叉树。由于堆是完全二叉树,所以利用数组来实现堆的空间利用率是最高的。

完全二叉树,


首先满二叉树是指,树上的每一个节点,其左子树高度==右子树高度。那么高度为h的完全二叉树就是前h-1层为满二叉树,最后一层节点从左向右连续出现。


image.png

完全二叉树的判断


由完全二叉树的逐层由左往右铺满的构造形态可以联想到利用树的层次遍历来判断完全二叉树。注意为了达到从左往右遍历铺满的目的,每一个节点的左孩子应该先于右孩子进入队列。假设当前出队的节点R(遍历到R),若在判断R的子节点时出现空子节点,则停止遍历,如果是完全二叉树,那么此时所有的节点都应进入过队列;否则必然不是完全二叉树。

// 伪代码
class Node {
    Node l, r;	// 左子节点,右子节点
    int value;
}
public boolean isFullBinaryTree(Node root, int treeSize) {
    Queue<Node> q = new LinkedList<>();
    int cnt = 1;
    q.add(root);
    while (!q.isEmty()) {
        Node u = q.poll();
        if (u.l == null) break;
        else {
            q.add(u.l);	cnt += 1;
        }
        if (u.r == null) break;
        else {
            q.add(u.r);	cnt += 1;
        }
    }
    return cnt == treeSize;
}


但是这种方法,需要提前知道树的大小,如果未给出,那么我们就还需要对树再进行一次遍历来获得二叉树的大小。这个过程似乎有些累赘,我们还可以继续改进下算法。注意到以上算法,我们是不将空节点入队的,所以在截断的时候可能并没有将所有的节点都遍历到(非完全二叉树的情况),我把这个叫做信息的缺失吧,正是由于这个信息的缺失需要我们再遍历一次树来获取完整信息,即这棵树的高度。


image.png


为了做进一步改进,我们把空也看作节点加入队列,直到当前节点为空才停止遍历。那么如果是完全二叉树,则队列中剩余的必然都是空节点;否则队列中必然有非空节点。我把这个叫做信息的过饱和,过多的信息往往能够帮助我们更好地判断,这样我们就只需要遍历一次二叉树。

public boolean isFullBinaryTree(Node root) {
	Queue<Node> q = new LinkedList<>();
	Node cur = root;
	while (cur != null) {
		q.add(cur.l);	q.add(cur.r);
		cur = cur.l;
	}
	while (!q.isEmpty()) {
		if (q.poll() != null)
			return false;
	} return true;
}

堆的判断


在二叉树是完全二叉树的基础上,我们来继续判断堆,只需要利用堆的局部有序性即可。就是遍历堆的每一个非叶子节点R,判断R的关键码是否大于等于其子节点。

// 伪代码
// 大小为heapSize的堆的最后一个下标为heapSize-1
// 则第一个分支节点是最后一个叶子节点的父亲 => 下标为(heapSize-1-1)/2
public boolean isHeap(int[] heap, int heapSize) {
    for (int i = (heapSize-2) >> 1; i >= 0; --i) {
        int l = (i<<1) + 1, r = l;
        if (l + 1 < heapSize) r = r + 1;
        if (heap[i] < heap[l] || heap[i] < heap[r])
            return false;
    } return true;
}

堆的维护


堆的维护操作关键点就是对节点进行上推下拉操作。上推下拉的最终目的就是把某一个节点移动到正确的位置

private void shiftDown(int[] arr, int curIndex, int size) {
    while (cur < size) {
        int left = (cur << 1) + 1, right = left + 1;
        if (right < size && arr[left] < arr[right]) // 为了统一与left交换
            left = right;
        if (left < size && arr[left] > arr[cur]) {
            swap(arr, left, cur);
            cur = left;
        } else break;
    }
}

// 维护堆的下拉操作
// 递归版本
private void shiftDown2(int[] arr, int cur, int n) {
    if (cur >= n) return;
    int lef = (cur << 1) + 1, rig = lef + 1;
    if (rig < n && arr[lef] < arr[rig]) 
        lef = rig;
    if (lef < n && arr[lef] > arr[cur]) {
        swap(arr, cur, lef);
        shiftDown2(arr, lef, n);
    }
}

private void shiftUp(int arr[], int cur, int n) {
    while (cur > 0) {
        int fath = (cur - 1) >> 1;
        if (arr[fath] < arr[cur]) {
            swap(arr, fath, cur);
            cur  = fath;
        }
    }
}

堆的插入


为了最小的开销,所以插入最初应该插入到叶子节点,然后对插入的叶子节点进行上推操作,上推到正确的位置。

private void insert(int arr[], int size, int val) {
    arr[size++] = val;
    shiftUp(arr, size - 1, size);
}

堆的根节点删除


为了最小的开销,根节点的删除其实就是将根节点与最后一个叶子节点交换位置之后,将堆的大小减一,接着把当前的新的根节点下拉到正确的位置。

private void pop() {
    // 交换根与最后叶子节点
    swap(arr, 0, size - 1);
    // 堆大小减一
    size -= 1;
    // 对根下拉
    shiftDown(arr, 0, size);
}

堆排序


堆排序就是利用了堆的根绝对最值的性质来实现。由于堆根节点的删除是将根与最后一个叶子节点交换,之后上推下拉维护大小减一的新堆。实际上每一次“删除”都将当前的最值放到了“最后”,所以要升序排序,应构建最大值堆:要降序排序,应构建最小值堆。

// 交换建堆法O(n),构建最大值堆
// 从第一个非叶子节点开始,进行下拉操作
public void make_heap(int arr[], int n) {
    for (int i = (n-2) >> 1 ; i >= 0; --i) {
        shift_down(arr, i, n);
    }
}

public void heap_sort(int[] arr, int n) {
    make_heap(arr, n);
    for (int i = n; i > 1; --i) {
        pop_heap();
    }
}
posted @ 2020-03-19 23:58  Bankarian  阅读(253)  评论(0编辑  收藏  举报