js数据结构与算法—— 二叉堆和堆排序

95 阅读3分钟

二叉堆

二叉堆是一种特殊的二叉树。

它的特性:

  • 是一棵完全二叉树。树的每一层都有左侧和右侧子节点,除了最后一层的叶节点。最后一层的叶准备节点尽可能都是左侧子节点,这是结构特性。
  • 二叉堆不是最小堆就是最大堆。最小堆可以快速导出数的最小值,最大堆可以快速导出数的最大值。所有的节点都大于等于(最大堆)或小于等于(最小堆)每个它的子节点。这是堆特性。

图示:

image.png

创建最小堆

首先我们来创建一个类:

const Compare = {
	LESS_THAN: -1,
	BIGGER_THAN: 1,
	EQUALS: 0,
};

function defaultCompare(a, b) {
	if (a === b) {
		return Compare.EQUALS;
	}
	return a < b ? Compare.LESS_THAN : Compare.BIGGER_THAN;
}

class MinHeap {
	constructor(compareFn = defaultCompare) {
		/**比较函数 */
		this.compareFn = compareFn;
		/**存储数据 */
		this.heap = [];
	}
}

二叉树的数组表示

二叉树的两种表示方式:

  • 用指针表示。
  • 使用一个数组通过索引值检索父节点、左侧和右侧子节点的值。

image.png

使用index访问数组表示的二叉树节点。

对于给定位置index的节点:

  • 它的左侧子节点的位置是:2 * index + 1(如果位置可用);
  • 它的右侧子节点的位置是:2 * index + 2(如果位置可用);
  • 它的父侧子节点的位置是:index / 2(如果位置可用);

在类中添加访问节特定节点方法:

	/**
	 * 获取当前位置的左节点位置
	 * @param {*} index
	 * @returns
	 */
	getLeftIndex(index) {
		return 2 * index + 1;
	}

	/**
	 * 获取当前位置的右节点位置
	 * @param {*} index
	 * @returns
	 */
	getRightIndex(index) {
		return 2 * index + 2;
	}

	/**
	 * 获取当前位置的右节点位置
	 * @param {*} index
	 * @returns
	 */
	getParentIndex(index) {
		if (index === 0) return undefined;
		return Math.floor((index - 1) / 2);
	}

向堆中插入值

向堆中插入值是指将值插入堆的底部叶节点(数组的最后一个位置)。再执行siftUp方法,表示我们将要将这个值和它的父节点进行交换,直到父节点小于这个插入的值。

向堆中插入新值的方法实现:

	/**
	 * 向堆中插入一个值
	 * @param {*} value
	 */
	insert(value) {
		if (value != null) {
			this.heap.push(value);
			this.siftUp(this.heap.length - 1);
			return true;
		}
		return false;
	}

siftUp方法实现:

	/**
	 * 将这个值和它的父节点进行交换,直到父节点小于这个插入的值
	 * @param {number} index 索引
	 */
	siftUp(index) {
		// 获取其父节点的位置
		let parent = this.getParentIndex(index);
		// 如果插入的值小于它的父节点(在最小堆中,或在最大堆中比父节点大),那么我们将这个元素和父节点交换。

		while (
			index > 0 &&
			this.compareFn(this.heap[parent], this.heap[index]) > Compare.EQUALS
		) {
			swap(this.heap, parent, index);
			index = parent;
			parent = this.getParentIndex(index);
		}
	}

siftUp方法接收插入值的位置作为参数。我们同样需要获取其父节点的位置。

如果插入的值小于它的父节点(在最小堆中,或在最大堆中比父节点大),那么我们将这个元素和父节点交换。我们重复这个过程,直到堆的根节点也经过了交换节点和父节点位置的操作。

交换方法如下:

/**
 * 交换数组中两个位置的值
 * @param {array} array
 * @param {number} a
 * @param {number} b
 */
function swap(array, a, b) {
	return ([array[a], array[b]] = [array[b], array[a]]);
}

插入节点示例:

const heap = new MinHeap();

heap.insert(2);
heap.insert(3);
heap.insert(4);
heap.insert(5);
heap.insert(1);

示意图:

image.png

从堆中找到最大值或最小值

在最小堆中,最小值总是位于数组的第一个位置(堆的根节点)。

代码如下:

	/**堆的大小 */
	size() {
		return this.heap.length;
	}
	/**是否为空 */
	isEmpty() {
		return this.size() === 0;
	}
    /**获取堆中的最小值。 */
	findMinimum() {
        // 如果堆不为空,我们返回数组的第一个值。
		return this.isEmpty() ? undefined : this.heap[0];
	}

获取堆中的最小值。

const heap = new MinHeap();

heap.insert(2);
heap.insert(3);
heap.insert(4);
heap.insert(5);
heap.insert(1);
console.log(heap.findMinimum());// =>1

注意: 在最大堆中,数组的第一个元素保存了最大值,所以我们可以使用相同的代码。

导出堆中的最大值或最小值

移除最小值(最小堆)或最大值(最大堆)表示移除数组中的第一个元素(堆的根节点)。

移除后将堆的最后一个元素移动至根部,并执行siftDown函数表示我们将交换元素,直到堆的结构正常。

代码如下:

	/**移除最小值(最小堆)或最大值(最大堆) */
	extract() {
		if (this.isEmpty()) return undefined;
		if (this.size() === 1) return this.heap.shift();

		const removedValue = this.heap.shift();
        this.heap.unshift(this.heap.pop())
		this.siftDown(0);
		return removedValue;
	}
	/**
	 * 交换元素,直到堆的结构正常
	 * @param {number} index 移除元素的位置
	 */
	siftDown(index) {
		let element = index;
		const left = this.getLeftIndex(index);
		const right = this.getRightIndex(index);
		const size = this.size();
		if (
			left < size &&
			this.compareFn(this.heap[element], this.heap[left]) > Compare.EQUALS
		) {
			element = left;
		}
		if (
			right < size &&
			this.compareFn(this.heap[element], this.heap[right]) >
				Compare.EQUALS
		) {
			element = right;
		}
		if (index !== element) {
			swap(this.heap, index, element);
            this.siftDown(element)
		}
	}

示例:

const heap = new MinHeap();

heap.insert(1);
heap.insert(2);
heap.insert(3);
heap.insert(4);
heap.insert(5);
console.log(heap.heap);// [1, 2, 3, 4, 5]
console.log(heap.extract());
console.log(heap.heap);// [2, 4, 3, 5]

创建最大堆

最大堆的算法和最小堆的算法一模一样,不同之处在于我们要把所有>的比较换成<的比较。

代码如下:

function reverserCompare(compareFn) {
	return (a, b) => compareFn(b, a);
}

class MaxHeap extends MinHeap {
	constructor(compareFn = defaultCompare) {
		super(compareFn);
		this.compareFn = reverserCompare(compareFn);
	}
}

const heap = new MaxHeap();
heap.insert(2)
heap.insert(3)
heap.insert(4)
heap.insert(5)
heap.insert(1)
console.log(heap);
console.log(heap.findMinimum());

最大堆的最大值是堆的根节点。

堆排序算法

堆排序算法实现步骤:

  1. 使用数组创建一个最大堆用作数据源。
  2. 创建最大堆后,最大的值会被存储在堆的第一个位置。接着将它替换为堆的最后一个值,将堆的大小减1。
  3. 最后将堆的根节点下移,并重复步骤2,直到堆的大小为1。

可用最大堆得到一个升序排列的数组(从最小到最大)。若想数组按降序排列,可用最小堆代替。

堆排序算法代码:

function heapify(array, index, heapSize, compareFn) {
  let largest = index;
  const left = (2 * index) + 1;
  const right = (2 * index) + 2;
  if (left < heapSize && compareFn(array[left], array[index]) > 0) {
    largest = left;
  }
  if (right < heapSize && compareFn(array[right], array[largest]) > 0) {
    largest = right;
  }
  if (largest !== index) {
    swap(array, index, largest);
    heapify(array, largest, heapSize, compareFn);
  }
}

function buildMaxHeap(array, compareFn) {
  for (let i = Math.floor(array.length / 2); i >= 0; i -= 1) {
    heapify(array, i, array.length, compareFn);
  }
  return array;
}

function heapSort(array, compareFn = defaultCompare) {
  let heapSize = array.length;
  buildMaxHeap(array, compareFn);
  while (heapSize > 1) {
    swap(array, 0, --heapSize);
    heapify(array, 0, heapSize, compareFn);
  }
  return array;
}