大根堆和堆排序的原理與實現

twilight0402發表於2020-12-01

shift up

  • 插入一個元素時,把元素追加到列表的最後,也就是堆的葉子節點上。然後執行shifup操作,對新元素進行從下往上的調整。
  • 判斷當前元素是否比父節點更大,如果是,則交換。否則就終止。
  • 因為插入一個元素時,列表已經是一個大根堆,所以當出現父元素大於自己時,就沒有必要繼續,因為父元素的父元素值更大。

shift down

  • 刪除一個元素時,把該元素和列表的最後一個元素交換。然後列表的長度減一(如果用count計數的話)。剩餘的元素進行shiftdown操作。

  • shiftdown,如果兩個孩子中存在比自己更大的元素,就和那個孩子交換值。然後這個孩子作為根,繼續這個操作。

  • 如下: 插入時執行shiftUp,刪除時執行shiftDown

// 堆排序
class Heap{
        private List<Integer> data;

        public Heap(){
            this.data = new ArrayList<>();
        }

        public void insert(int value){
            this.data.add(value);
            this.shiftUp(this.data.size() - 1);
        }
        public int pop(){
            this.swap(0, this.data.size() - 1);
            int res = this.data.remove(this.data.size() - 1);
            this.shiftDown(0);
            return res;
        }

        /**
         * 上移,元素加入時執行
         * @param k
         */
        public void shiftUp(int k){
            // 0 元素沒有父元素
            // 當出現k比父元素小的時候,就沒有必要繼續下去,因為,父元素的父元素一定是更大的數
            while(k >= 1 && data.get(k) > data.get((k - 1) / 2)){
                swap(k, (k - 1) / 2);
                k = (k - 1) / 2;
            }
        }

        /**
         * 下移,調整堆時執行
         * @param k
         */
        public void shiftDown(int k){
            // 省略了對左孩子的判斷
            while(2 * k + 1 < data.size()){
                int child =  2 * k + 1;
                if(child + 1 < this.data.size() && this.data.get(child) < this.data.get(child + 1)){
                    child += 1;
                }
                if(this.data.get(child) > this.data.get(k)){
                    this.swap(k, child);
                }
                k = child;
            }

        }

        public void swap(int left, int right){
            int temp = this.data.get(left);
            this.data.set(left, this.data.get(right));
            this.data.set(right, temp);
        }
    }
    
    // main
        _215_陣列中的第k個最大元素.Heap2 heap = handle.new Heap2();
        // 建立堆
        for(int item : list){
            heap.insert(item);
        }
        for(int item : list){
            System.out.print(heap.pop() + " ");
        }

heapify

  • 不需要一個一個的插入元素,可以直接對原陣列的所有非葉節點進行heapify,即可構成一個大根堆。
  • ((size - 1) - 1) / 2 表示最大下標減一。當然,用size/2也行,多出的幾個元素都是葉子節點,都可以當成是隻有一個元素的堆。
        public Heap2(List<Integer> list){
            this.data = new ArrayList<>(list);
            heapify();
        }
        public void heapify(){
            for(int i = (data.size() - 1 - 1) / 2; i >= 0; --i){
                shiftDown(i);
            }
        }

就地排序

  • 這裡的nums陣列的資料從0開始拍,這樣的話,按照如下方式計算父子節點
    • 左孩子: index * 2 + 1
    • 右孩子: index * 2 + 2
    • 父節點: (index - 1) / 2 或者 (length -1 -1)/2。有的演算法直接用 (length / 2)也不會有錯,因為最右邊的葉子節點都可以看成是單個的大根堆。
class Heap {
        public void heapSort(int [] nums){
            // 從第一個非葉節點開始,執行shiftdown操作
            int size = nums.length;

            // 建立一個堆
            // 很多攜程(size-1)/2 或者 size/2都行
            for( int i = (size -1 - 1)/2; i >= 0; --i){
                shiftDown(nums, i, size);
            }
            // 從堆中取元素
            for (int i = size - 1; i > 0; i --){
                swap(nums, 0, i);           // i 元素被放到0位置
                shiftDown(nums, 0, i);      // 重新堆0位置的元素shiftdown
            }
        }

        /**
         * 對第i個元素執行下移操作。大根堆
         * @param nums
         * @param i
         */
        public void shiftDown(int [] nums, int k, int size){
            // 用while 少了一次迴圈,避免了遞迴
            while( 2 * k + 1 < size){
                int child = 2 * k + 1;      // 左孩子
                if ( child + 1 < size && nums[child+1] > nums[child])
                    child += 1;             // 右孩子

                if (nums[k] < nums[child]){
                    swap(nums, child, k);
                }

                k = child;                  // 從被交換的孩子開始,繼續往下迭代。直到到達葉子節點
            }
        }

        public void swap(int[] a, int i, int j) {
            int temp = a[i];
            a[i] = a[j];
            a[j] = temp;
        }
    }

相關文章