0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

アルゴリズムとデータ構造(java)

Last updated at Posted at 2025-01-05

基本的なデータ構造とアルゴリズムの基礎知識

本記事では、バイナリーサーチ(Binary Search)、バイナリーツリー(Binary Tree)、ヒープ(Heap)、スタック(Stack)、キュー(Queue)について、それぞれの概要と平均計算量をオーダー記法(Big O記法)で解説します。また、各データ構造やアルゴリズムのJava実装例も紹介します。

バイナリーサーチ(Binary Search)

概要

バイナリーサーチは、ソートされた配列に対して効率的に要素を探索するアルゴリズムです。データを半分に分割し、探索対象の値と中央の値を比較することで、探索範囲を逐次的に半減させていきます。反復的(イテレーティブ)および再帰的な実装が可能です。

平均計算量

  • 時間計算量: O(log n)

バイナリーサーチは、探索範囲を半分に絞り込む操作を繰り返すため、データのサイズnに対してlog n回の比較が必要です。したがって、平均的なケースではO(log n)の時間計算量となります。ただし、データがソートされている必要があります。

バイナリーサーチのJava実装

反復的実装

public class BinarySearchIterative {
    /**
     * バイナリサーチ(反復的)
     * ソートされた配列内で指定された値を探索し、見つかったインデックスを返す。
     * 見つからない場合は -1 を返す。
     *
     * @param numbers ソートされた整数配列
     * @param value   探索する値
     * @return 見つかったインデックスまたは -1
     */
    public static int binarySearchIterative(int[] numbers, int value) {
        int left = 0;
        int right = numbers.length - 1;

        while (left <= right) {
            int mid = left + (right - left) / 2; // オーバーフローを防ぐ
            if (numbers[mid] == value) {
                return mid;
            } else if (numbers[mid] < value) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }

        return -1;
    }

    public static void main(String[] args) {
        int[] nums = {0, 1, 5, 7, 9, 11, 15, 20, 24};
        int valueToSearch = 15;

        int result = binarySearchIterative(nums, valueToSearch);
        System.out.println("Binary Search Iterative Index: " + result); // 出力: 6
    }
}

再帰的実装

public class BinarySearchRecursive {
    /**
     * バイナリサーチ(再帰的)
     * ソートされた配列内で指定された値を探索し、見つかったインデックスを返す。
     * 見つからない場合は -1 を返す。
     *
     * @param numbers ソートされた整数配列
     * @param value   探索する値
     * @return 見つかったインデックスまたは -1
     */
    public static int binarySearchRecursive(int[] numbers, int value) {
        return _binarySearchRecursive(numbers, value, 0, numbers.length - 1);
    }

    private static int _binarySearchRecursive(int[] numbers, int value, int left, int right) {
        if (left > right) {
            return -1;
        }

        int mid = left + (right - left) / 2; // オーバーフローを防ぐ
        if (numbers[mid] == value) {
            return mid;
        } else if (numbers[mid] < value) {
            return _binarySearchRecursive(numbers, value, mid + 1, right);
        } else {
            return _binarySearchRecursive(numbers, value, left, mid - 1);
        }
    }

    public static void main(String[] args) {
        int[] nums = {0, 1, 5, 7, 9, 11, 15, 20, 24};
        int valueToSearch = 15;

        int result = binarySearchRecursive(nums, valueToSearch);
        System.out.println("Binary Search Recursive Index: " + result); // 出力: 6
    }
}

バイナリーツリー(Binary Tree)

概要

バイナリーツリーは、各ノードが最大で2つの子ノード(左子と右子)を持つ階層型データ構造です。探索、挿入、削除が効率的に行えるため、多くのアルゴリズムやデータ構造の基盤として利用されます。特に、**二分探索木(Binary Search Tree, BST)**は、ソートされたデータの格納と高速な検索を可能にします。

平均計算量

  • 探索、挿入、削除: O(log n)

バイナリーツリーがバランスされている場合、各操作はツリーの高さに依存し、平均的なケースではO(log n)の時間計算量となります。ただし、バランスが崩れた場合(例えば、すべてのノードが一方向に偏っている場合)は、最悪ケースでO(n)となります。

二分探索木のJava実装

class TreeNode {
    int value;
    TreeNode left, right;

    public TreeNode(int item) {
        value = item;
        left = right = null;
    }
}

public class BinarySearchTree {
    TreeNode root;

    BinarySearchTree() {
        root = null;
    }

    /**
     * 二分探索木への挿入
     *
     * @param root  現在のノード
     * @param value 挿入する値
     * @return 挿入後のノード
     */
    TreeNode insert(TreeNode root, int value) {
        if (root == null) {
            root = new TreeNode(value);
            return root;
        }

        if (value < root.value)
            root.left = insert(root.left, value);
        else if (value > root.value)
            root.right = insert(root.right, value);

        return root;
    }

    /**
     * 中間順巡回(In-order Traversal)
     *
     * @param root 現在のノード
     */
    void inorder(TreeNode root) {
        if (root != null) {
            inorder(root.left);
            System.out.print(root.value + " ");
            inorder(root.right);
        }
    }

    public static void main(String[] args) {
        BinarySearchTree tree = new BinarySearchTree();

        /* 二分探索木を構築
              50
           /      \
         30        70
         /  \     /  \
       20   40  60   80 */
        tree.root = tree.insert(tree.root, 50);
        tree.insert(tree.root, 30);
        tree.insert(tree.root, 20);
        tree.insert(tree.root, 40);
        tree.insert(tree.root, 70);
        tree.insert(tree.root, 60);
        tree.insert(tree.root, 80);

        // 中間順巡回の結果を表示
        tree.inorder(tree.root); // 出力: 20 30 40 50 60 70 80 
    }
}

ヒープ(Heap)

概要

ヒープは、完全二分木を基にしたデータ構造で、特に**最大ヒープ(Max Heap)や最小ヒープ(Min Heap)**が知られています。ヒープは優先度キューの実装やヒープソート、グラフアルゴリズムなど、さまざまな場面で利用されます。最大ヒープでは親ノードが子ノードよりも大きい値を持ち、最小ヒープではその逆となります。

平均計算量

  • 挿入、削除: O(log n)

ヒープに要素を挿入したり削除したりする際には、ヒープの性質を維持するために再ヒープ化(ヒープの再構築)が必要で、これにはツリーの高さに比例する時間がかかります。完全二分木であるため、ツリーの高さはlog nです。

最大ヒープ(Max Heap)のJava実装

public class MaxHeap {
    private int[] heap;
    private int size;
    private int maxsize
        // コンストラクタ
    public MaxHeap(int maxsize) {
        this.maxsize = maxsize;
        this.size = 0;
        heap = new int[this.maxsize + 1];
        heap[0] = Integer.MAX_VALUE; // 1-based indexing
    }

    // 親ノードのインデックス
    private int parent(int pos) {
        return pos / 2;
    }

    // 左子ノードのインデックス
    private int leftChild(int pos) {
        return 2 * pos;
    }

    // 右子ノードのインデックス
    private int rightChild(int pos) {
        return 2 * pos + 1;
    }

    // ノードの交換
    private void swap(int fpos, int spos) {
        int tmp;
        tmp = heap[fpos];
        heap[fpos] = heap[spos];
        heap[spos] = tmp;
    }

    // ヒープに挿入
    public void insert(int element) {
        if (size >= maxsize) {
            System.out.println("Heap is full. Cannot insert " + element);
            return;
        }
        heap[++size] = element;
        int current = size;

        // ヒープの性質を維持するために上昇
        while (heap[current] > heap[parent(current)]) {
            swap(current, parent(current));
            current = parent(current);
        }
    }

    // ヒープから最大値を削除
    public int extractMax() {
        if (size == 0) {
            System.out.println("Heap is empty.");
            return -1;
        }
        int popped = heap[1];
        heap[1] = heap[size--];
        maxHeapify(1);
        return popped;
    }

    // ヒープの性質を維持するためのメソッド
    private void maxHeapify(int pos) {
        if (pos >= (size / 2) && pos <= size)
            return;

        if (heap[pos] < heap[leftChild(pos)] ||
            heap[pos] < heap[rightChild(pos)]) {

            if (heap[leftChild(pos)] > heap[rightChild(pos)]) {
                swap(pos, leftChild(pos));
                maxHeapify(leftChild(pos));
            } else {
                swap(pos, rightChild(pos));
                maxHeapify(rightChild(pos));
            }
        }
    }

    // ヒープの内容を表示
    public void printHeap() {
        for (int i = 1; i <= size / 2; i++) {
            System.out.print(
                " PARENT : " + heap[i] +
                " LEFT CHILD : " + heap[2 * i] +
                " RIGHT CHILD :" + heap[2 * i + 1]);
            System.out.println();
        }
    }

    public static void main(String[] args) {
        MaxHeap maxHeap = new MaxHeap(15);

        /* ヒープを構築
              10
           /      \
         20        30
        /  \      /  \
      40   50   60   70 */
        maxHeap.insert(10);
        maxHeap.insert(20);
        maxHeap.insert(30);
        maxHeap.insert(40);
        maxHeap.insert(50);
        maxHeap.insert(60);
        maxHeap.insert(70);

        // ヒープの内容を表示
        maxHeap.printHeap();

        // 最大値を抽出
        System.out.println("The max val is " + maxHeap.extractMax()); // 出力: 70
        maxHeap.printHeap();
    }
}

スタック(Stack)

概要

スタックは、**後入れ先出し(LIFO: Last-In-First-Out)**のデータ構造です。基本的な操作として、要素の追加(プッシュ)と削除(ポップ)があり、直近に追加された要素が最初に取り出されます。スタックは関数呼び出しの履歴管理や式の評価、バックトラック処理などに利用されます。

平均計算量

  • プッシュポップ: O(1)

スタックの基本操作であるプッシュとポップは、常に定数時間で実行できます。

スタックのJava実装

配列を用いたスタックの実装

public class Stack {
    private int maxSize;
    private int[] stackArray;
    private int top;

    // コンストラクタ
    public Stack(int size) {
        maxSize = size;
        stackArray = new int[maxSize];
        top = -1;
    }

    // プッシュ操作
    public void push(int value) {
        if (isFull()) {
            System.out.println("Stack is full. Cannot push " + value);
            return;
        }
        stackArray[++top] = value;
    }

    // ポップ操作
    public int pop() {
        if (isEmpty()) {
            System.out.println("Stack is empty.");
            return -1;
        }
        return stackArray[top--];
    }

    // ピーク操作(トップの要素を取得)
    public int peek() {
        if (isEmpty()) {
            System.out.println("Stack is empty.");
            return -1;
        }
        return stackArray[top];
    }

    // スタックが空かどうかをチェック
    public boolean isEmpty() {
        return (top == -1);
    }

    // スタックが満杯かどうかをチェック
    public boolean isFull() {
        return (top == maxSize - 1);
    }

    public static void main(String[] args) {
        Stack stack = new Stack(5);

        stack.push(10);
        stack.push(20);
        stack.push(30);
        stack.push(40);
        stack.push(50);
        stack.push(60); // 満杯のためプッシュできない

        System.out.println("Top element is " + stack.peek());

        System.out.println("Popped element: " + stack.pop());
        System.out.println("Popped element: " + stack.pop());

        stack.push(60); // 空きができたのでプッシュ可能
        System.out.println("Top element is " + stack.peek());
    }
}

キュー(Queue)

概要

キューは、**先入れ先出し(FIFO: First-In-First-Out)**のデータ構造です。基本的な操作として、要素の追加(エンキュー)と削除(デキュー)があり、最初に追加された要素が最初に取り出されます。キューはプリントジョブの管理や幅優先探索(BFS)、リアルタイムデータ処理などに利用されます。

平均計算量

  • エンキューデキュー: O(1)

キューの基本操作であるエンキューとデキューは、常に定数時間で実行できます。

キューのJava実装

配列を用いたキューの実装

public class Queue {
    private int maxSize;
    private int[] queueArray;
    private int front;
    private int rear;
    private int currentSize;

    // コンストラクタ
    public Queue(int size) {
        maxSize = size;
        queueArray = new int[maxSize];
        front = 0;
        rear = -1;
        currentSize = 0;
    }

    // エンキュー操作
    public void enqueue(int value) {
        if (isFull()) {
            System.out.println("Queue is full. Cannot enqueue " + value);
            return;
        }
        rear = (rear + 1) % maxSize;
        queueArray[rear] = value;
        currentSize++;
    }

    // デキュー操作
    public int dequeue() {
        if (isEmpty()) {
            System.out.println("Queue is empty.");
            return -1;
        }
        int value = queueArray[front];
        front = (front + 1) % maxSize;
        currentSize--;
        return value;
    }

    // ピーク操作(フロントの要素を取得)
    public int peek() {
        if (isEmpty()) {
            System.out.println("Queue is empty.");
            return -1;
        }
        return queueArray[front];
    }

    // キューが空かどうかをチェック
    public boolean isEmpty() {
        return (currentSize == 0);
    }

    // キューが満杯かどうかをチェック
    public boolean isFull() {
        return (currentSize == maxSize);
    }

    public static void main(String[] args) {
        Queue queue = new Queue(5);

        queue.enqueue(10);
        queue.enqueue(20);
        queue.enqueue(30);
        queue.enqueue(40);
        queue.enqueue(50);
        queue.enqueue(60); // 満杯のためエンキューできない

        System.out.println("Front element is " + queue.peek());

        System.out.println("Dequeued element: " + queue.dequeue());
        System.out.println("Dequeued element: " + queue.dequeue());

        queue.enqueue(60); // 空きができたのでエンキュー可能
        System.out.println("Front element is " + queue.peek());
    }
}

まとめ

以下に、今回紹介したデータ構造とアルゴリズムの平均計算量をまとめます。

データ構造・アルゴリズム 平均時間計算量
バイナリーサーチ O(log n)
バイナリーツリー 探索・挿入・削除: O(log n)
ヒープ 挿入・削除: O(log n)
スタック プッシュ・ポップ: O(1)
キュー エンキュー・デキュー: O(1)
  • バイナリーサーチは、ソートされた配列に対して高速な探索を可能にし、O(log n)の時間計算量を持ちます。
  • バイナリーツリーや**二分探索木(BST)**は、効率的なデータ管理と操作を可能にし、探索や挿入、削除が平均的にO(log n)で行えます。
  • ヒープは、優先度キューの実装やヒープソートなどに利用され、挿入と削除がO(log n)の時間で行えます。
  • スタックキューは基本的なデータ構造であり、特定の操作に対して常に定数時間で対応可能です。

参考資料

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?