機械学習エンジニアの備忘録

主に自分が勉強したことのメモ

Priority Queues (Cousera Data Structure week3)

CouseraのData Strucureコースのweek3の内容です。
www.coursera.org

前回のweek2の内容の記事はこちら
rikeiin.hatenablog.com

Priority Queues (優先度付きキュー)

Priority Queueは通常のキューに格納されている要素に優先度がついたデータ構造である。
C++ではpriority_queue、JavaではPriorityQueue、Pythonではheapqとして実装されている。
典型的な用途としてはジョブのスケジューリングなどがあげられる。

priority queueは以下の操作を持っている。

  • Insert(p):新しい要素を優先度pで挿入する
  • ExtractMax():優先度が最大の要素を取り出す

オプションで以下の操作を定義することもある

  • Remove(it):イテレータitでポイントされた要素を削除する
  • GetMax():要素のセットは変えずに優先度が最大の要素を返す
  • ChangePriority(it, p):イテレータitでポイントされた要素の優先度をpに変える

単純な実装

ソートされていない配列・リスト

f:id:rikeiin:20180415135651p:plain

  • Insert(e)
    • 要素eを末尾に追加する
    • 実行時間はO(1)
  • ExtractMax()
    • 配列・リストを走査する
    • 実行時間はO(n)

ソートされた配列

f:id:rikeiin:20180415140333p:plain

  • ExtractMax()
    • 最後の要素を取り出す
    • 実行時間はO(1)
  • Insert(e)
    • 要素eの場所を探す(二分探索でO(log n))、右の要素を一つずつづらし(O(n))、挿入する(O(1))
    • 実行時間:O(n)

ソートされたリスト

f:id:rikeiin:20180415141534p:plain

  • ExtractMax()
    • 最後の要素を取り出す
    • 実行時間:O(1)
  • Insert(e)
    • 要素eの場所を探し(二分探索が使えないため、O(n))、挿入(O(1))
    • 実行時間:O(n)

実行時間まとめ

f:id:rikeiin:20180415141825p:plain

Priority Queues: Binary Heaps(バイナリーヒープ)

Binary max-heapの定義は子より親のほうが値が大きい場合の二分木である。
以下がヒープである例
f:id:rikeiin:20180415142706p:plain

ヒープではない例
f:id:rikeiin:20180415142741p:plain

基本的なヒープに対する操作

GetMax

f:id:rikeiin:20180415142940p:plain

Insert

新しい要素を木の末端の葉にくっつける。
f:id:rikeiin:20180415143357p:plain

この操作において子より親の値が大きくなればならないという条件を満たしていな場合、以下のSiftUpという操作を条件が満たすまで行う。

SiftUp

シフトアップは子の大きい値と親の小さい値を入れ替えるという操作であり、木がヒープの条件を満たすまで繰り返す。
実行時間はO(木の高さ)である。

ExtractMax

木の根と葉を入れ替えることで行う。
f:id:rikeiin:20180415144715p:plain

上の木に対してExtractMaxを行うと以下のようになる。
f:id:rikeiin:20180415144819p:plain

当然これはヒープの条件を満たしていないため、以下のSiftDownという操作を行う。

SiftDown

SiftDownは親の小さい値と子の大きいを入れ替えてノードを下にずらす操作である。
上の図の例では12と29を入れ替える。このとき左右の子の大きいほうと入れ替えること。
この操作をヒープの条件を満たすまで繰り返す。
もちろん実行時間はO(木の大きさ)

ChangePriority

任意のノードを任意の値に変える。その後にヒープの条件を満たすようにSiftUpかSiftDownを繰り返す。

Remove

削除したいノードの値を∞にかえる。
f:id:rikeiin:20180415150027p:plain

その後、ヒープの条件を満たすようにSiftUpを繰り返す(最終的に∞が根に来る)。
その後はExtractMaxと同じ操作を行う。
実行時間はO(木の高さ)

ヒープ操作のまとめ

  • GetMaxはO(1)だが他の操作はO(木の高さ)
  • 木は出来る限り浅くしたい

どうやって木を浅くするか?

Complete binary tree の定義は最後の深さ以外のノードが全て埋まっており、最後の深さのノードは左から順に埋まっていることである。

Complete binary tree の例:
f:id:rikeiin:20180417172103p:plain:w500

Complete binary tree ではない例:
f:id:rikeiin:20180417172155p:plain:w500

n個のノードを持つcomplete binary treeの深さは最大でO(log n)
complete binary treeのメリットととして以下のように配列で表現できる。

f:id:rikeiin:20180417174103p:plain:w600

complete binary tree をキープしておくために

  • 新しい要素をInsertするときは最後の深さで左から順に挿入する
  • ExtractMaxするときはルートノードと最後の葉と入れ替える

f:id:rikeiin:20180417174658p:plain:w200

Complete Binary Tree まとめ

  • すべての操作がO(log n)で動作して高速である(GetMaxはO(1))
  • 配列で保持しておけるのでメモリ空間の効率がよい
  • 実装が簡単

Heap Sort

Heap Sortのやり方

  1. ヒープ構造を作る
  2. ExstraMax()を繰り返す
ヒープ構造の作り方
  • 下から上に向かっていくようにヒープ構造を修正していく
  • 最初は深さ1のサブツリーから修正していき、ルートまでたどり着いたときにすべてのサブツリーに対してヒープ構造が保たれている
  • 参考URL:Heap Sort Visualization
  • SiftDownをO(n)ノードに行うため計算量はO(nlog n)
  • ノードが葉に近ければShifDownは高速に行える
ヒープソートまとめ
  • PriorityQueueは主にInsertとExtractMaxという操作を持つ
  • リストと配列による実装は片方の操作はとても高速(O(1))だがもう片方の操作はとても遅い(O(n))
  • バイナリーヒープによる実装は両方の操作がO(log n)で行え、かつメモリスペース的にも効率がよい

記事が長くなってしまったのでDisjoint Setに関する内容は別記事にします。

追記:練習がてらにpythonヒープソートを行うコードを書いてみました。

import numpy as np
import random

class HeapBuilder(object):

    def __init__(self, array_length):
        self._heap = np.random.permutation(np.arange(1,array_length+1))
        self.size = len(self._heap)
    def _parent(self, i):
        return (i - 1) // 2

    def _leftChild(self, i):
        return 2 * i + 1

    def _rightChild(self, i):
        return 2 * i + 2

    def _swap(self, i, j):
        temp = self._heap[i]
        self._heap[i] = self._heap[j]
        self._heap[j] = temp

    def _siftUp(self, i):
        while (i > 0) and (self._heap[self._parent(i)] < self._heap[i]):
            self._swap(i, self._parent(i))
            i = self._parent(i)
            
    def _siftDown(self, i, size):
        maxindex = i
        l = self._leftChild(i)
        if (l < size) and (self._heap[l] > self._heap[maxindex]):
            maxindex = l
        
        r = self._rightChild(i)
        if (r < size) and (self._heap[r] > self._heap[maxindex]):
            maxindex = r

        if i != maxindex:
            self._swap(i, maxindex)
            self._siftDown(maxindex, size)

    def buildHeap(self):
        for i in reversed(range(0, self._parent(self.size))):
            self._siftDown(i, self.size)

    def heapSort(self):
        self.buildHeap()
        n = self.size
        for _ in range(self.size):
            self._swap(0, n - 1)
            n -= 1
            self._siftDown(0, n)
    def showHeap(self):
        print (self._heap)

if __name__ == '__main__':
    heap = HeapBuilder(1000)
    heap.showHeap()
    heap.heapSort()
    heap.showHeap()