數據結構之堆(我猜,關於堆的這些維護細節,你肯定不清楚,不信你來看!)

  在前面分析了二叉搜索樹紅黑樹等衆多樹結構,今天博主給大家換個口味,深入分析一下的實現原理與維護規則。(其實與二叉樹有點相似)

一、的概述

1、什麼是

  ,在日常生活中是一個量詞,比如:一堆木頭,下面這張圖就是一堆木頭,大家注意它的擺放規則,成金字塔型(上窄下寬)。
在這裏插入圖片描述
  數據結構中的,與上面的結構類似,不過藉助了二叉樹的結構。肯定會有小夥伴來一句,woc,這不就是二叉樹麼。。。其實你也可以暫時這麼理解,看完博客你就會發現還是有些區別。
在這裏插入圖片描述

2、的劃分

  根據中元素擺放順序的不同,可分爲兩種堆結構,小頂堆大頂堆

小頂堆:對於堆中的任意節點A,如果它存在子節點B、C,則 節點A的值 ≤ min{子節點B的值, 子節點C的值}

大頂堆:對於堆中的任意節點A,如果它存在子節點B、C,則 節點A的值 ≥ max{子節點B的值, 子節點C的值}
在這裏插入圖片描述
\color{red}注意:堆只對父節點的值與子節點的值有大小限定,但是對於節點左、右子節點的值相對關係沒有限定(二叉搜索樹的特徵纔是 左子節點 < < 右子節點)。

3、的作用

  講了半天,也畫了好幾張圖,辣麼有什麼作用呢?

  根據前面小頂堆大頂堆的定義,我們知道小頂堆堆頂存放的是堆中最小的元素,大頂堆堆頂存放的堆中最大的元素。而這正是的作用,可能會有小夥伴一臉鄙夷,就這找最值的功能都要特意設計一個數據結構來實現?

  那來一道面試題,給你10億個數,如何在最短的時間裏找出最大的10個? (我覺得如果沒做出這道題,辣麼只能說了解堆的定義,但是不理解、不會運用,邊看博客,邊思考吧,文末附答案)

二、的底層實現

  的實現一般使用數組,而不是二叉樹什麼的。從形態來看,堆不就是棵二叉樹麼,爲啥不用二叉樹而用數組呢?主要原因是爲了隨機訪問通過下標訪問元素)。本篇博客將只討論使用數組實現的,如果你偏要用二叉樹,可以自己實現一個。

  以數組實現不僅帶來了隨機訪問通過下標訪問元素),其實還有兩個很重要的規律

  1. 下標爲index的左、右孩子的下標分別是index * 2 + 1(index + 1) * 2
  2. 堆中有子節點的節點最大下標爲 size / 2 - 1(注意:size爲堆的大小,不是數組的大小)

在這裏插入圖片描述

三、的維護

  首先說明一下,一般對外展示的只有堆頂,也就是說插入移除,在外界看來都是在堆頂操作。(下面的維護都是基於小頂堆大頂堆的維護操作是類似的。)

1、擴容

  如果數組沒有剩餘空位置,此時需要對數組進行擴容。由於基於數組實現的只有數組這個結構,只是通過數組下標邏輯上存在,所以只要將數組中的元素按原來的順序複製到一個更長的數組即可。
在這裏插入圖片描述

2、插入元素(上浮)

  對於插入操作,我們首先將元素放到下一個空閒的位置,然後對插入的元素進行上浮操作。
在這裏插入圖片描述
插入元素上浮僞代碼如下:

// data是堆數組,size是堆的大小(並不是data數組的長度),element是待插入的節點
void insert(int data[], int &size, int element) {
	// 假設data有空位置插入,將待插入元素直接放到堆尾的下一個空位置
	int insertIndex = size++;
	data[insertIndex] = element;
	// 上浮插入節點
	while (insertIndex > 0) {
		// 求出父節點的下標(根據父節點下標能求出左右子節點下標,那麼根據子節點下標同樣也能求出父節點下標)
		int parentIndex = (insertIndex - 1) / 2;
		// 如果插入的元素大於父節點
		if (data[insertIndex] > data[parentIndex]) {
			// 上浮完畢
			break;
		}
		// 否則插入的元素小於父節點元素的值,交換
		swap(data[insertIndex], data[parentIndex]);
		// 更新插入元素的下標,繼續上浮
		insertIndex = parentIndex;
	}
}

3、刪除(堆頂)元素(下沉)

  插入元素是將尾端元素上浮,而刪除堆頂元素,是將堆尾元素放入堆頂,然後將該元素下沉
在這裏插入圖片描述

刪除元素下沉僞代碼如下:

// data是堆數組,size是堆的大小(並不是data數組的長度)
void deleteTop(int data[], int &size) {
	if (size == 0) {
		// 堆爲空,無需進行刪除操作
		return;
	}
	// 將堆尾替換堆頂
	data[0] = data[--size];
	// 記錄需要下沉節點的下標,尋找最大的有子節點的下標(前說過這是一條規律)
	int index = 0, lastHaveChildIndex = size / 2 - 1;
	// 只要當data[index]存在子節點纔有下沉的必要
	while (index >= lastHaveChildIndex) {
		// 右子節點必定有左子節點(左子節點的公式: 父節點下標 * 2 + 1)
		// minChildIndex用於記錄左、右子節點更小的下標
		int minChildIndex = index * 2 + 1, rightChildIndex;
		if ((rightChildIndex = index * 2 + 2) < size && data[minChildIndex] > data[rightChildIndex]) {
			// 如果index存在右子節點,並且右子節點的值比左子節點的值小,更新minChildIndex
			minChildIndex = rightChildIndex;
		}
		// 如果下沉節點data[index]比左右子節點最小值都大,停止下沉
		if (data[index] >= data[minChildIndex]){
			break;
		}
		// 否則與左、右子節點較小則交換,並且更新index,繼續下沉
		swap(data[index], data[minChildIndex]);
		index = minChildIndex;
	}
}

四、總結

  堆中插入元素,直接放入數組的下一個空位置,然後上浮插入元素,即可完成堆的調整;刪除堆頂元素,將堆尾的元素替換堆頂,再對堆頂的元素進行下沉,即可完成堆的調整。(再次強調一下,基於數組實現的,只有數組這個結構,只是根據數組下標的依賴關係在邏輯上存在。如果你將堆的實現修改爲二叉樹,那麼纔是真正存在。)

  現在提下前面提出的面試題的答案,給你10億個數,如何在最短的時間裏找出最大的10個?
  我們只要構建一個大小爲10的小頂堆,前期取10個數直接插入堆中,然後對剩下的10億-10個數,每此取出一個都與堆頂(堆中最小值)比較,如果比堆頂大,則替換堆頂,接着調整堆(下沉堆頂元素即可),最終堆中的元素就是10億個數中最大的10個數。(如果答案都沒看懂,再看一遍博客吧,有點走馬觀花哦)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章