アルゴリズム・データ構造 プログラミング

[データ構造(Ruby)]スタック・キュー・木構造の解説

データ構造キュー・スタック・木構造

こんにちは!じゃいごテックのあつしです。

今回はデータ構造の基本、スタックやキュー、木構造についてご紹介します。また、優先度付きキューの仕組みについても解説したいと思います。

スタック

スタック(stack) は、データが入ってきた順に並べて後に入れたデータから順番に取り出す規則で作られたデータ構造で、後に入れたものを先に出すので後入れ先出し ( LIFO: Last In First Out ) と呼ばれます。
スタックにデータを入れることをプッシュ(push)データを取りだすことをポップ(pop)といいます。

1冊ずつ本を積んで行って、上から読んでいくイメージです。

スタックのサンプルコード

Rubyでは配列に対して push -> pushメソッドpop -> popメソッドスタックを実装することが出来ます。

stack = []

stack.push(11)
stack.push(22)
stack.push(33)

p stack
# > [11, 22, 33]
p stack.pop
# > 33
p stack.pop
# > 22
p stack.pop
# > 11
  • pushメソッドで配列末尾から 11, 22, 33 を順番に挿入します。
  • popメソッドで配列末尾から要素を順番に取り出します。

push: pushメソッドで末尾側から要素を挿入する

  • stack-push11
  • stack-push22
  • stack-push33

pop: popメソッドで末尾側から要素を取り出す

  • stack-pop33
  • stack-pop22
  • stack-pop11

キュー

キュー(queue) は、データが入ってきた順に並べて先に入れたデータから順番に取り出す規則で作られたデータ構造です。
先に入れたものから先に出すので先入れ先出し( FIFO: First In First Out )と呼ばれます。
キューにデータを入れることをエンキュー(enqueue)データを取りだすことをデキュー(dequeue)といいます。

身近な例でいうとプリンタの印刷待ちや、レジの順番待ちです。

キューのサンプルコード

Rubyでは配列に対して enqueue -> pushメソッドdequeue -> shiftメソッドキューを実装することが出来ます。

queue = []
queue.push(11)
queue.push(22)
queue.push(33)

p queue
# > [11, 22, 33]
p queue.shift
# > 11
p queue.shift
# > 22
p queue.shift
# > 33
  • pushメソッドで配列末尾から 11, 22, 33 を順番に挿入します。
  • shiftメソッドで配列先頭から要素を順番に取り出します。

enqueue: pushメソッドで末尾側から要素を挿入する

  • queue-enqueue11
  • queue-enqueue22
  • queue-enqueue33

dequeue: shiftメソッドで末尾側から要素を取り出す

  • queue-dequeue11
  • queue-dequeue22
  • queue-dequeue33

両端キュー

両端キュー(double-ended queue または deque) は、データ列の先頭・末尾のどちらからでもデータを追加・取り出しできるデータ構造です。

ビーズアクセサリを作るイメージです。

両端キューのサンプルコード

Rubyでは配列に対して 先頭へ追加 -> unshiftメソッド先頭から取り出し -> shiftメソッド末尾への追加 -> pushメソッド末尾から取り出し -> popメソッドで実装することが出来ます。

deque = []

deque.push(11)
deque.push(22)
deque.unshift(33)
deque.unshift(44)
deque.unshift(55)

p deque
# > [55, 44, 33, 11, 22]
p deque.pop
# > 22
p deque.pop
# > 11
p deque.pop
# > 33
p deque.shift
# > 55
p deque.shift
# > 44
  • pushメソッドで配列末尾から 11, 22 を順番に挿入します。
  • ubshiftメソッドで配列先頭から 33, 44, 55 を順番に挿入します。
  • popメソッドで配列末尾から3個の要素を順番に取り出します。
  • shiftメソッドで配列先頭から2個の要素を順番に取り出します。

pushメソッドで末尾側から要素を挿入する

  • deque-push11
  • deque-push22

unshiftメソッドで先頭側から要素を挿入する

  • deque-unshift33
  • deque-unshift44
  • deque-unshift55

popメソッドで末尾側から要素を取り出す

  • deque-pop22
  • deque-pop11
  • deque-pop33

shiftメソッドで先頭側から要素を取り出す

  • deque-shift55
  • deque-shift44

木構造

木構造とは、1つの要素が複数の子要素をもち、さらに子要素が複数の孫要素をもつような形で、下位層が深くなるほど枝分かれしていくデータ構造のことです。

ノード(node) : 節(枝がわかれるところ)です。

根ノード(root node) : 親を持たないノード、頂点ノードです。

葉ノード(leaf node) : 子を持たないノード、末端ノードです。

エッジ(edge) : ノードとノードを結ぶリンクです。

二分木(binary tree) 子の数が2個以下になっている木構造です。

ヒープ(heap) : 木構造のうち、親要素が常に子要素より大きい(又は小さい)という条件を満たす構造です。

二分ヒープ(binary heap) : 二分木ヒープの条件を満たした構造です。

00_木構造

優先度付きキュー

データを優先度付きで追加し、優先度が高いものから取り出すデータ構造です。

優先度の異なるタスクが沢山あって、処理中にも新しいタスクが入ってるが、常に優先度の高いものから処理するようなイメージです。

優先度付きキューサンプルコード

※ Aizu Online Judgeで優先度付きキューを実装する課題 AOJ ALDS1_9_C がありますので、同じ仕様で実装してみます。

class PriorityQueue
  def initialize
    @data = []
  end

  # 要素追加
  def insert(element)
    @data.push(element)
    up_heap
  end

  # 要素取り出し
  def extract
    return @data.pop if @data.length <= 1
    target_element = @data[0]
    @data[0] = @data.pop
    down_heap
    target_element
  end

  private

  # ノード交換
  def swap(a, b)
    @data[a], @data[b] = @data[b], @data[a]
  end

  # 親 index
  def parent_idx(idx)
    idx / 2 + idx % 2 - 1
  end

  # 左の子 index
  def left_child_idx(idx)
    (idx * 2) + 1
  end

  # 右の子 index
  def right_child_idx(idx)
    (idx * 2) + 2
  end

  # 子が存在するか?
  def has_child?(idx)
    ((idx * 2) + 1) < @data.size
  end

  # 葉から根方向 ノード比較・交換
  def up_heap
    idx = @data.length - 1
    return if idx == 0
    parent_idx = parent_idx(idx)
    while (@data[parent_idx] < @data[idx])
      swap(parent_idx, idx)
      return if parent_idx == 0
      idx = parent_idx
      parent_idx = parent_idx(idx)
    end
  end

  # 根から葉方向 ノード比較・交換
  def down_heap
    idx = 0
    while (has_child?(idx))
      l_idx = left_child_idx(idx)
      r_idx = right_child_idx(idx)
      if @data[r_idx].nil?
        target_idx = l_idx
      else
        target_idx = @data[l_idx] >= @data[r_idx] ? l_idx : r_idx
      end
      if @data[idx] < @data[target_idx]
        swap(idx, target_idx)
        idx = target_idx
      else
        return
      end
    end
  end
end

pqueue = PriorityQueue.new

pqueue.insert(33)
pqueue.insert(11)
pqueue.insert(22)
pqueue.insert(55)
pqueue.insert(44)
pqueue.insert(66)

p pqueue
# > <PriorityQueue:0x00007fffe5d2cb38 @data=[66, 44, 55, 11, 33, 22]>
p pqueue.extract
# > 66
p pqueue.extract
# > 55
p pqueue.extract
# > 44
p pqueue.extract
# > 33
p pqueue.extract
# > 22
p pqueue.extract
# > 11

pq01

insert(33)

  • ノードを追加します。
  • 最初のノードなので比較・交換はありません。

pq02

insert(11)

  • 末尾[1] にノードを追加します。
    (ノード[0]の左の子ノードになる)
  • 親(ノード[0])より値が小さいのでノード交換はありません。

pq03

insert(22)

  • 末尾[2] にノードを追加します。
    (ノード[0]の右の子ノードになる)
  • 親(ノード[0])より値が小さいのでノード交換はありません。

pq04-1

insert(55)

  • 末尾[3] にノードを追加します。
    (ノード[1]の左の子ノードになる)

 


pq04-2

  • ノード[3](子)とノード[1](親)を比較します。
  • ノード[3](子)がノード[1](親)より値が大きいのでノード[3]とノード[1]を交換します。

pq04-3

  • ノード[1](子)とノード[0] (親)を比較します。
  • ノード[1](子)がノード[0](親)より値が大きいのでノード[1]とノード[0]を交換します。
  • 根(ノード[0])まで比較したので終了。

pq05-1

insert(44)

  • 末尾[4] にノードを追加します。
    (ノード[1]の右の子ノードになる)

 


pq05-2

  • ノード[4](子)とノード[1](親)を比較します。
  • ノード[4](子)がノード[1](親)より値が大きいのでノード[4]とノード[1]を交換します。
  • ノード[1](子)とノード[0](親)を比較します。
  • ノード[1](子)はノード[0](親)より値が小さいので比較終了

pq06-1

insert(66)

  • 末尾[5] にノードを追加します。
    (ノード[2]の右の子ノードになる)

pq06-2

  • ノード[5](子)とノード[2](親)を比較します。
  • ノード[5](子)がノード[2](親)より値が大きいのでノード[5]とノード[2]を交換します。

pq06-3

  • ノード[2](子)とノード[0] (親)を比較します。
  • ノード[2](子)がノード[0](親)より値が大きいのでノード[2]とノード[0]を交換します。
  • 根(ノード[0])まで比較したので終了。

pq06-4

挿入が完了した状態

二分木かつヒープなので、二分ヒープ構造(Binary heap)となっています。


pq07-1

extract

  • 根ノードを参照して値 (66) を取り出します。
  • 末尾ノード[5] を pop で取り出し、根ノードを上書きします。
  • 子のノードのうち値が大きいノード[2] と 比較します。

pq07-2

  • ノード[0](親)よりノード[2](子)が値が大きいのでノード[0]とノード[2]を交換します。
  • 葉(ノード[2])まで比較したので終了。

pq08-1

extract

  • 根ノードを参照して値 (55) を取り出します。
  • 末尾ノード[4]をpopで取り出し、根ノードを上書きします。
  • 子のノードのうち値が大きいノード[1] と比較します。

pq08-2

  • ノード[0](親)よりノード[1](子)が値が大きいのでノード[0]とノード[1]を交換します。
  • ノード[1](親)とノード[3](左の子)を比較すると、親の方が大きいので終了。

pq09-1

extract

  • 根ノードを参照して値 (44) を取り出します。
  • 末尾ノード[3]をpopで取り出し、根ノードを上書きします。
  • 子のノードのうち値が大きいノード[1] と 比較します。

pq09-2

  • ノード[0](親)よりノード[1](子)が値が大きいのでノード[0]とノード[1]を交換します。
  • 葉(ノード[1])まで比較したので終了。

pq10

extract

  • 根ノードを参照して値 (33) を取り出します。
  • 末尾ノード[2]をpopで取り出し、根ノードを上書きします。
  • ノード[0](親)がノード[1](子)よりが値が大きいので終了

pq11

extract

  • 根ノードを参照して値 (22) を取り出します。
  • 末尾ノード[2]をpopで取り出し、根ノードを上書きします。
  • 子ノードがないので比較はありません。

pq12

extract

  • 要素数1なのでpopで取り出して終了

今回のまとめ

スタックは深さ優先探索(depth-first search: DFS)、キューは幅優先探索(breadth first search: BFS)に使われます。また、両端キューや木構造もアルゴリズムを実装するときに便利です。プログラミング言語によっては標準ライブラリに含まれていますので調べて使ってみて下さい!

データ構造を理解すると、アルゴリズムを考えるときに良いアイデアが浮かんだりしますのでじっくり取り組んでみて下さい!

【PR】Ruby学習でお世話になった本




【PR】アルゴリズム学習でお世話になった本


アルゴリズム関連の技術書は大抵C/C++で書かれていますが、ある程度プログラムが出来れば、処理の流れは理解することが出来ます。






通称: 螺旋本。C++で解説されています。AOJ(Aizu Online Judge)を運営している会津大学の渡辺准教授が書いた本です。データ構造や計算量の内容から丁寧に書いてありますのでアルゴリズム学習の最初の参考書としてオススメです。







通称: 蟻本。C++で解説されています。競技プログラミング中級者の定番書と言われていて、競技プログラミングで利用できる色々なアルゴリズムを学ぶことが出来ます。かなり高度な内容も含まれているので1冊分を完全に理解するのにはかなりの時間がかかりそうですが、手元に置いて何度も読み返しています。







通称: チーター本。C#, C++, JAVAでTopcoderの問題を解説しています。初心者~中級者向けに書かれているので解説が非常に丁寧です。







Python3でアルゴリズムを解説している本です。講義スタイルの本で、図やフローチャートを使ってアルゴリズムとデータ構造についてしっかりと説明されています。代わりにコードは少なめです。Pythonコードが読めなくても十分理解できると思います。


-アルゴリズム・データ構造, プログラミング
-, , , , , ,

© 2024 じゃいごテック