heo-jae-won@home:~$

スタックとキュー

実装ースタック

  • Stackを実現するためには、LIFO仕組みを実装する必要がある
  • 最初を配列には型パラメータに指定することができないことをわからなかったため、その部分だけ修正してもらった
  • Javaの場合は、最後の要素をクリアする前にサイズを調節するのだが、私はGoLangと同じくしといた
  • サイズを0にしたので、インデックスみたいに見えるかもしれないけど、これは容量として実装されてる
    • サイズが0から始まるのは、最初の要素の数は0だからだ
import java.util.Arrays;

public class Stack<E> {
    private E[] elements;
    private int size = 0;
    private static final int DEFAULT_SIZE = 16;

    public Stack() {
        this.elements = (E[]) new Object[DEFAULT_SIZE]; // new E[DEFAULT_SIZE] impossible
    }

    public void push(E e) {
        ensureCapacity();
        elements[size++] = e;
    }

    private void ensureCapacity() {
        if (size < elements.length) {
            return;
        }
        elements = Arrays.copyOf(elements, 2 * size + 1);
    }

    public E pop() {
        if (size == 0) {
            throw new IllegalStateException("Stack is empty");
        }

        int lastIndex = size - 1;
        E element = elements[lastIndex];
        elements[lastIndex] = null; 
        size--;

        /*java version.
        int lastIndex = --size;
        E element = elements[lastIndex];
        elements[lastIndex] = null; // prevent memory leak
        */

        return element;
    }
}
  • Golangでは配列ではなく、動的な配列であるSliceを使う
  • なのでJavaと同じく実装する必要はない
    • サイズみたいな変数は消した
type Stack[T any] struct {
    elements []T
}

func (stack *Stack[T]) Push(e T) {
    stack.elements = append(stack.elements, e)
}

func (stack *Stack[T]) Pop() (T, bool) {
    var zero T

    if len(stack.elements) == 0 {
        return zero, false
    }

    lastIndex := len(stack.elements) - 1
    e := stack.elements[lastIndex]
    stack.elements[lastIndex] = zero
    stack.elements = stack.elements[:lastIndex]

    return e, true
}


func (stack *Stack[T]) Peek() (T, bool) {
    var zero T

    if len(stack.elements) == 0 {
        return zero, false
    }

    return stack.elements[len(stack.elements)-1], true
}
  • mainでの活用は以下のようになる
  • Javaとは完全に違う文法を使っているため、慣れるに時間が非常にかかった
func main() {
    var s Stack[int]

    s.Push(10)
    s.Push(20)

    // Peek
    if val, ok := s.Peek(); ok {
        fmt.Println("Top:", val)
    } else {
        fmt.Println("Stack is empty")
    }

    // Pop
    if val, ok := s.Pop(); ok {
        fmt.Println("Popped:", val)
    }

    if val, ok := s.Pop(); ok {
        fmt.Println("Popped:", val)
    }

    // Empty case
    if val, ok := s.Pop(); !ok {
        fmt.Println("Nothing to pop:", val) // val is zero value (0 for int)
    }
}

実装ースタックで文字列を反転させる

  • Stackを利用した例
Stack<Character> stack = new Stack<>();

for (char c : "string".toCharArray()) {
    stack.push(c);
}

StringBuilder strBuilder = new StringBuilder();

while (stack.getSize() > 0) {
    strBuilder.append(stack.pop());
}

String reverse = strBuilder.toString();
System.out.println(reverse);

実装ーキュー

  • 最初に私が実現したQueueはStackとほぼ同じ仕組みだった
  • 当時はサーキュラーバッファという概念を知らなかった
  • ただ、ensureCapacity()をスタックと同じくしたから、poll() が O(n) になってしまい、データが大量になると、性能が悪化する
import java.util.Arrays;

public class Queue<E> {
    private E[] elements;
    private int size = 0;
    private static final int DEFAULT_SIZE = 16;

    public Queue() {
        this.elements = (E[]) new Object[DEFAULT_SIZE]; 
    }

    public void push(E e) {
        ensureCapacity();
        elements[size++] = e;
    }

    private void ensureCapacity() {
        if (size < elements.length) {
            return;
        }
        elements = Arrays.copyOf(elements, 2 * size + 1);
    }

    public E poll() {
        if (size == 0) {
            throw new IllegalStateException("Stack is empty");
        }

        E element = elements[0];

        //drag all value in front of one element
        for (int i = 1; i < elements.length; i ++) {
            elements[i-1] = elements[i];
        }
        elements[--size] = null; 

        return element;
    }
}
  • そのため、以下のようにロジックを直した
  • 空間を確保するために、新しい配列を作る ensureCapacity()を呼び出す
  • 以前にヌルとかになった空間は全部前にずれて、0からインデックスが始まらせる
public class Queue<E> {
    private E[] elements;
    private int head = 0;
    private int tail = 0;
    private int size = 0;
    private static final int DEFAULT_SIZE = 16;

    public Queue() {
        elements = (E[]) new Object[DEFAULT_SIZE];
    }

    public void push(E e) {
        ensureCapacity();
        elements[tail] = e;
        tail = (tail + 1) % elements.length;
        size++;
    }

    public E poll() {
        if (size == 0) {
            throw new IllegalStateException("Queue is empty");
        }

        E e = elements[head];
        elements[head] = null;
        head = (head + 1) % elements.length;
        size--;

        return e;
    }

    private void ensureCapacity() {
        if (size < elements.length) return;

        E[] newArr = (E[]) new Object[elements.length * 2];

        for (int i = 0; i < size; i++) {
            newArr[i] = elements[(head + i) % elements.length];
        }

        elements = newArr;
        head = 0;
        tail = size;
    }
}
  • Go言語に聞いたら、以下の通りだった
  • append()ではなく q.elements[q.tail] = e のように直接代入している理由は、あらかじめ capacity(容量)を確保しているためである
    • append()を使うと、確保した初期容量の後ろに要素が追加されてしまい、head と tail の意味がなくなってしまう
    • また、q.tail = (q.tail + 1) % len(q.elements) とすることで、与えられた容量内で循環する(サーキュラーバッファとして動作する)ようにしている
  • head と tail は単なる現在の位置を示すポインターではない
    • head は「読み出し位置」を指すインデックス
    • tail は「書き込み位置」を指すインデックス
  • ただ、これは容量が調節できない
type Queue[T any] struct {
    elements []T
    head     int
    tail     int
    size     int
}

func NewQueue[T any](capacity int) *Queue[T] {
    return &Queue[T]{
        elements: make([]T, capacity),
    }
}

func (q *Queue[T]) Push(e T) bool {
    if q.size == len(q.elements) {
        return false // full
    }

    q.elements[q.tail] = e
    q.tail = (q.tail + 1) % len(q.elements)
    q.size++
    return true
}

func (q *Queue[T]) Poll() (T, bool) {
    var zero T
    if q.size == 0 {
        return zero, false
    }

    result := q.elements[q.head]
    q.elements[q.head] = zero // important for GC
    q.head = (q.head + 1) % len(q.elements)
    q.size--

    return result, true
}
  • 容量調節ができないのは、実戦で使えないので、調節も可能にする
func (q *Queue[T]) resize(newCapacity int) {
    newElements := make([]T, newCapacity)

    // Copy in correct order starting from head
    for i := 0; i < q.size; i++ {
        newElements[i] = q.elements[(q.head+i)%len(q.elements)]
    }

    q.elements = newElements
    q.head = 0
    q.tail = q.size
}

func (q *Queue[T]) Push(e T) bool {
    // Resize when full
    if q.size == len(q.elements) {
        newCap := len(q.elements) * 2
        if newCap == 0 {
            newCap = 1
        }
        q.resize(newCap)
    }

    q.elements[q.tail] = e
    q.tail = (q.tail + 1) % len(q.elements)
    q.size++
    return true
}

func (q *Queue[T]) shrinkIfNeeded() {
    if len(q.elements) <= 1 {
        return
    }

    if q.size <= len(q.elements)/4 {
        q.resize(len(q.elements) / 2)
    }
}

func (q *Queue[T]) Poll() (T, bool) {
    var zero T
    if q.size == 0 {
        return zero, false
    }

    result := q.elements[q.head]
    q.elements[q.head] = zero
    q.head = (q.head + 1) % len(q.elements)
    q.size--

    q.shrinkIfNeeded()

    return result, true
}

深く探究ーデキューで文字列を反転させる

  • ただのキューだけじゃ、時間計算量が非効率になる
  • そのため、Dequeを作成して文字列を反転させる
type Deque[T any] struct {
	buf      []T
	head     int
	tail     int
	size     int
	capacity int
}

func NewDeque[T any](cap int) *Deque[T] {
	if cap < 1 {
		cap = 1
	}
	return &Deque[T]{
		buf:      make([]T, cap),
		capacity: cap,
	}
}

func (d *Deque[T]) resize() {
	newCap := d.capacity * 2
	newBuf := make([]T, newCap)

	for i := 0; i < d.size; i++ {
		newBuf[i] = d.buf[(d.head+i)%d.capacity]
	}

	d.buf = newBuf
	d.head = 0
	d.tail = d.size
	d.capacity = newCap
}

func (d *Deque[T]) PushBack(v T) {
	if d.size == d.capacity {
		d.resize()
	}

	d.buf[d.tail] = v
	d.tail = (d.tail + 1) % d.capacity
	d.size++
}

func (d *Deque[T]) PushFront(v T) {
	if d.size == d.capacity {
		d.resize()
	}

	d.head = (d.head - 1 + d.capacity) % d.capacity
	d.buf[d.head] = v
	d.size++
}

func (d *Deque[T]) PopFront() T {
	if d.size == 0 {
		panic("empty deque")
	}

	v := d.buf[d.head]

	var zero T
	d.buf[d.head] = zero // avoid memory leak

	d.head = (d.head + 1) % d.capacity
	d.size--

	return v
}

func (d *Deque[T]) PopBack() T {
	if d.size == 0 {
		panic("empty deque")
	}

	d.tail = (d.tail - 1 + d.capacity) % d.capacity
	v := d.buf[d.tail]

	var zero T
	d.buf[d.tail] = zero

	d.size--

	return v
}

func (d *Deque[T]) Size() int {
	return d.size
}

func ReverseString(s string) string {
	d := NewDeque[rune](len(s))

	for _, ch := range s {
		d.PushBack(ch)
	}

	result := make([]rune, 0, len(s))

	for d.Size() > 0 {
		result = append(result, d.PopBack())
	}

	return string(result)
}

深く探究ーかっこバリデーション

  • かっこが一個入ってくると、ペアになるかっこも入ってくるはずだと思ったが、どうすればペアで管理ができるのかが全然わからなかった
  • 結局これは解決できず、ChatGPTに聞いた
  • 以下はJava版だが、ペアで管理する発想には至らなかった
  • Mapというデータ型をこう利用する方法があると勉強になった
import java.util.*;

public class BracketValidator {

    public static boolean isValid(String s) {
        Deque<Character> stack = new ArrayDeque<>();

        Map<Character, Character> pairs = Map.of(
            ')', '(',
            '}', '{',
            ']', '['
        );

        for (char ch : s.toCharArray()) {

            if (ch == '(' || ch == '{' || ch == '[') {
                stack.push(ch);

            } else if (ch == ')' || ch == '}' || ch == ']') {

                if (stack.isEmpty()) {
                    return false;
                }

                char top = stack.pop();

                if (top != pairs.get(ch)) {
                    return false;
                }
            }
        }

        return stack.isEmpty();
    }

    public static void main(String[] args) {
        System.out.println(isValid("()[]{}")); // true
        System.out.println(isValid("(]"));     // false
        System.out.println(isValid("([)]"));   // false
        System.out.println(isValid("{[]}"));   // true
    }
}

深く探究ー言語のAPIを使うのが推薦される理由

  • 以下のようにQueueのPollメソッドを実現した
public E poll() {
    if (size == 0) {
        throw new IllegalStateException("Stack is empty");
    }

    E element = elements[0];

    //drag all value in front of one element
    for (int i = 1; i < elements.length; i ++) {
        elements[i-1] = elements[i];
    }
    elements[--size] = null; 

    return element;
}
  • elements.length は容量(capacity)なので、実際には使用されていない領域までループしてしまう
  • そのため、実際に使用されている要素数を表す size を使うようにする
for (int i = 1; i < size; i++) {
    elements[i - 1] = elements[i];
}
  • 上とやることは全く同じだが、以下がNativeMethodでLow-levelに近いからより速い
System.arraycopy(elements, 1, elements, 0, size - 1);

深く探究ーvar s Stack[int]とvar s *Stack[int]の違い

  • 下記の場合を仮定してみると、
type Stack[T any] struct {
    data []T
}
  • var s Stack[int]は、s.data = nil になる
  • コンストラクタが不要
  • newやmakeなども不要
  • cuz “Zero value should be usable without initialization” is go’s goal
  • ただし、var s *Stack[int]は、s = nil になる
    • つまり、構造体が存在しないということだ
    • 従って、s.Push(10) を実行しても、s が nil なので、panicに陥る
      • あれはJavaで例えば、null.Pushと同じ
var s Stack[int]
s.data = append(s.data, 10)

x := s.data[0]
x = 20

fmt.Println(s.data[0]) // still 10
  • そのため、実質的に下記の組み合わせで使うことになる
  • これは、Tの容量が小さいとき、状態を共有する必要がないとき、すなわち、一般的によく使われる
type Stack[T any] struct {
    data []T
}

var s Stack[int]
s.data = append(s.data, 10)

x := s.data[0]
x = 20

fmt.Println(s.data[0]) 
  • 一方、下記の場合を仮定してみると、
type Stack[T any] struct {
    data []*T
}
  • 値を格納するためには、全てをポインター化して入れることになる
  • 値を取り出すときは、間接参照して値を取り出すことになる
s := Stack[int]{}
val := 10
s.data = append(s.data, &val)

*s.data[0] = 20 //間接参照(dereferencing)

fmt.Println(*s.data[0]) 

振り返り

  • 効率に優れるキューを実装するために、サーキュラーバッファーの形で実装する必要があるという考えに及ばなかった
  • そのため、headとtailというポインターの役割を担当する架空の概念が思いつかなかった
head int
tail int
  • head と tailの意味をちゃんと知らなかった
  • そのため、q.elements[q.elements.head + 1] = e のように式を作成した
  • headが読み出すところ、tailが書き込むところだとわかってからは、以下を理解した
q.elements[q.elements.head] = e 
  • サーキュラーバッファーという意味がよくわからなくて、append()で要素を追加してしまった
  • サーキュラーバッファーという意味がよくわからなくて、ただの tail++ でtail値を計算してしまった
    • 確保した容量の中で循環させるためには、配列の長さで割り算の余りを取る必要がある。
q.elements[q.tail] = e
tail = (tail + 1) % len(q.elements);