演算法(4)資料結構:堆

hard_man發表於2019-04-08

1.0 問題描述

實現資料結構:堆。

2.0 問題分析

  1. 堆一般使用陣列來表示,其中某個節點下標i的兩個子節點的下標為 2i+1 和 2i+2。堆是一棵完全二叉樹。
  2. 堆有3種基本操作:建立,插入,刪除。
  3. 這3種操作都需要通過“調整堆”的方式來實現。調整堆是指,對堆中的某個節點,若它的值和它所有子節點相比,不是最大/最小,那麼就需要將最大/最小的元素和當前節點交換,這種操作成為“調整堆”。
  4. 建立可以用插入來實現(複雜度O(nlogn)),也可以使用陣列直接轉換成堆(複雜度O(n))。
  5. 假設陣列長度為n,那麼這個陣列轉成堆,需要從第一個有子節點的節點((n - 1)/2或(n - 2)/2開始,倒序“調整堆”,直到下標為0。
  6. 插入操作可以先將資料插入到陣列尾部,然後依次調整該資料的父節點,父節點的父節點......直到根節點。
  7. 刪除操作可以先將陣列首尾節點互換,然後刪除最後面的元素,最後再調整根節點即可。

3.0 程式碼實現

3.1使用swift實現

/*
 * 堆型別
 * small 小根堆
 * big 大根堆
 */
enum HeapType {
    case small, big
}

/*
 * 堆
 */
class Heap<T: Comparable> {
    private var _arr: [T];
    private var _type: HeapType;
    
    /*
     * 使用陣列建立堆
     * @param {[T]} - arr 建立堆的陣列
     * @param {HeapType} - type 堆型別
     */
    init(arr: [T], type: HeapType = .big) {
        self._arr = arr;
        self._type = type;
        self._create();
    }
    
    /*
     * 調整堆:調整以idx為頂的3元素子堆,若頂部元素不是最大/最小,則調整為最大/最小
     * @param {Int} - idx 表示調整以idx為頂的3元素子堆
     * @return {Bool} - 調整成功返回true,無需調整返回false
     */
    @discardableResult
    private func _adjust(_ idx: Int) -> Bool{
        let maxAdjustIdx = Int(ceilf(Float(_arr.count) / 2) - 1);
        if idx > maxAdjustIdx || idx < 0{
            return false;
        }
        let leftIdx = 2 * idx + 1;
        let rightIdx = 2 * idx + 2;
        let top: T? = _arr[idx];
        let left: T? = leftIdx < _arr.count ? _arr[leftIdx]: nil;
        let right: T? = rightIdx < _arr.count ? _arr[rightIdx]: nil;
        if let t = top {
            if let l = left, let r = right {
                let v = self._type == .big ? max(t, l, r) : min(t, l, r);
                switch v {
                case l:
                    swapArr(arr: &_arr, f: leftIdx, t: idx);
                    _adjust(leftIdx);
                    return true;
                case r:
                    swapArr(arr: &_arr, f: rightIdx, t: idx);
                    _adjust(rightIdx);
                    return true;
                default:
                    break;
                }
            }else if let l = left {
                let b = self._type == .big ? (l > t) : (l < t);
                if b {
                    swapArr(arr: &_arr, f: leftIdx, t: idx);
                    _adjust(leftIdx);
                    return true;
                }
            }else if let r = right {
                let b = self._type == .big ? (r > t) : (r < t);
                if b {
                    swapArr(arr: &_arr, f: rightIdx, t: idx);
                    _adjust(rightIdx);
                    return true;
                }
            }
        }
        return false;
    }
    
    /**
     * 建立堆,依次調整 n/2 -> 0 元素
     */
    private func _create(){
        let maxAdjustIdx = Int(ceilf(Float(_arr.count) / 2) - 1);
        for i in stride(from: maxAdjustIdx, to: -1, by: -1){
            _adjust(i);
        }
    }
    
    /**
     * 彈出一個元素,移除頂部元素,然後令頂部元素等於最後一個元素,最後調整頂部元素
     * @return [T?] 返回頂部元素
     */
    func pop() -> T? {
        let first = _arr.first;
        if first != nil {
            if _arr.count <= 1 {
                _arr.removeLast();
            }else{
                swapArr(arr: &_arr, f: 0, t: _arr.count - 1);
                _arr.removeLast();
                _adjust(0);
            }
        }
        return first;
    }
    
    /**
     * 插入一個元素,插入到尾部,依次調整該元素的頂部元素,直到無需調整或下標為0
     * @param {T} - t 插入的元素
     */
    func push(_ t: T){
        _arr.append(t);
        var idx = Int(ceilf(Float(_arr.count - 1) / 2) - 1);
        while true {
            if !_adjust(idx){
                break;
            }
            if idx == 0{
                break;
            }
            idx = Int(ceilf(Float(idx) / 2) - 1);
        }
    }
    
    /**
     * 返回當前元素數量
     * @return {Int} 返回堆內元素數量
     */
    func count() -> Int{
        return _arr.count;
    }
}
複製程式碼

3.2使用js實現

//常量
const BigHeap = 1;
const SmallHeap = 2;

//建構函式
function Heap(type, arr, compareFunction){
    this.type = type;
    this.arr = arr;
    this.compareFunction = compareFunction;
    this._createHeap(arr);
}

//陣列交換
Heap._swap = function(i,j,arr){
    let tmp = arr[i];
    arr[i] = arr[j];
    arr[j] = tmp;
}

//比較值
Heap.prototype._compare = function(v1, v2){
    if(this.type == SmallHeap){
        if(v2 == null){
            return true;
        }else{
            if(this.compareFunction){
                return this.compareFunction(v1, v2) == -1;
            }else{
                return v1 < v2;
            }
        }
    }else{
        if(v2 == null){
            return true;
        }else{
            if(this.compareFunction){
                return this.compareFunction(v1, v2) == 1;
            }else{
                return v1 > v2;
            }
        }
    }
}

//調整堆的第i個結點
Heap.prototype._adjustNode = function(i, arr){
    let leftChildIdx = 2 * i + 1;
    let leftValue = null;
    if(leftChildIdx < arr.length){
        leftValue = arr[leftChildIdx];
    }
    let rightChildIdx = 2 * i + 2;
    let rightValue = null;
    if(rightChildIdx < arr.length){
        rightValue = arr[rightChildIdx];
    }
    if(!leftValue && !rightValue){
        return;
    }
    let exchangeIdx = null;
    //左值存在並且大於根結點
    if(leftValue && this._compare(leftValue, arr[i])){
        //右值存在並且大於左結點
        if(rightValue && this._compare(rightValue, leftValue)){
            //右值交換
            exchangeIdx = rightChildIdx;
        }else{
            //左值交換
            exchangeIdx = leftChildIdx;
        }
    }else if(rightValue && this._compare(rightValue, leftValue)){
        //右值交換
        exchangeIdx = rightChildIdx;
    }

    if(exchangeIdx != null){
        Heap._swap(exchangeIdx, i, arr);
        //交換完畢後,新的子結點也需要調整
        this._adjustNode(exchangeIdx, arr);
    }
}

//根據陣列建立堆
Heap.prototype._createHeap = function(arr){
    let len = arr.length;
    if(len <= 1){
        return;
    }
    //最後一個非葉子結點
    let lastNonLeafIdx = Math.floor(len / 2) - 1;
    //依次調整每個結點
    for (let index = lastNonLeafIdx; index >= 0; index--) {
        this._adjustNode(index, arr);
    }
}

//插入
Heap.prototype.insert = function(ele){
    this.arr.push(ele);
    let adjustIdx = this.arr.length;
    while(adjustIdx > 0){
        adjustIdx = Math.ceil(adjustIdx / 2) - 1;
        this._adjustNode(adjustIdx, this.arr);
        if(adjustIdx <= 0){
            break;
        }
    }
}

//刪除
Heap.prototype.remove = function(){
    if(this.arr.length <= 0){
        return null;
    }
    let value = this.arr[0];
    if(this.arr.length > 1){
        this.arr[0] = this.arr.pop();
        this._adjustNode(0, this.arr);
    }else{
        this.arr.pop();
    }
    return value;
}

//是否為空
Heap.prototype.empty = function(){
    return this.arr.length == 0 || this.arr[0] == null;
}
複製程式碼

4.0 複雜度分析

  1. 陣列轉堆的複雜度
    • 首先需要進行n/2次堆的調整
    • 每次調整堆最多可能需要 logn次的二次調整
      • 所以時間複雜度小於 O(nlogn)
    • 每次調整堆最少可能需要1次調整
      • 所以時間複雜度大於O(n/2)
    • 實際上,需要少量調整的運算元遠遠大於需要多次調整的操作。
      • 根據調整規則,設最高高度為h = logn
      • 根節點需要調整次數為:2^0*h
      • 第二層需要調整次數為:2^1*(h-1)
      • ... ...
      • 第logn層需要調整次數為:2^(h-1)*1
      • 總次數count=2^0*h + 2^1*(h-1) + 2^2*(h-2) + ... + 2^(h-1)*(h-(h-1)) //使用錯位相減
      • count*2 = 2^1*h + 2^2*(h-1) + 2^3*(h-2) + ... + 2^h*(h-(h-1))
      • count = count*2-count = 2^1 + 2^2 + ... + 2^(h-1) + 2^h - 2^0*h
      • count = 2^(h+1) - 2 - h
      • count = 2n - 2 - logn
      • 所以時間複雜度為O(n)
  2. 堆的插入:O(logn)
  3. 堆的刪除:O(logn)

相關文章