データ構造のスタックとキュー - アルゴリズムとデータ構造の入門 (4)

CSDNロゴ役職

この記事は、アルゴリズムとデータ構造に関する学習ノートの第 4 部であり、継続的に更新されます。友達と一緒に読んで勉強してください。わからないことや間違っていることがあれば、ご連絡ください

スタック

スタックは、一端の要素の挿入と削除のみを許可する線形データ構造です。データの挿入および削除操作の終端をスタックの最上位 (Top) と呼び、もう一方の終端をスタックの最下位 (Bottom) と呼びます。スタック内のデータ要素は、後入れ先出しLIFO (Last In First Out) の原則に従います。つまり、最後に入力された要素が最初にアクセスされます。

プッシュ (プッシュ): スタックの挿入操作はプッシュ/プッシュ/プッシュと呼ばれ、受信データはスタックの先頭にあります。
スタック (ポップ): スタックの削除操作はポップと呼ばれ、データもスタックの最上位にあります。

次のアニメーションは、スタックの内外をより直感的に理解できます。

ここに画像の説明を挿入

スタックの特徴

  1. 後入れ先出し (LIFO): 最後にスタックに入った要素が最初にアクセスされ、最初にスタックに入った要素が最後にアクセスされます。
  2. 挿入と削除は一方の端でのみ許可されます。スタックでは通常、スタックの先頭でのみ挿入と削除が許可され、それ以外の場所では許可されません。
  3. スタックのトップ ポインタ: スタックのトップ ポインタはスタックのトップ要素を指し、要素が挿入および削除されると変化します。

スタックアプリケーション

  1. 関数呼び出し: プログラミング言語の関数呼び出しプロセスでは、スタックを使用して関数のリターン アドレス、パラメーター、ローカル変数などの情報を格納します。関数が呼び出されるたびに、その関連情報がスタックにプッシュされ、関数の実行が終了すると、情報がスタックからポップされ、制御は呼び出し元の関数に戻ります。
  2. 式の評価: スタックは式の評価において重要な役割を果たします。式は、中置式を後置式に変換し、スタックを使用してオペランドと演算子を格納することによって評価されます。
  3. 括弧の一致: スタックは、式内の括弧が一致するかどうかを確認するためによく使用されます。式をトラバースするときに、左括弧に遭遇すると、その式がスタックにプッシュされます。右括弧に遭遇すると、スタックの最上位の要素と一致します。一致する場合は、スタックの最上位の要素がポップされ、トラバースは続行され、一致しない場合は括弧も一致しません。
  4. 元に戻す操作: テキスト エディターやグラフィック処理ソフトウェアでは、スタックを使用して元に戻す操作を実装できます。操作が実行されるたびに、関連する情報がスタックにプッシュされ、ユーザーが操作を元に戻す必要がある場合、スタックの最上位の要素がポップされて前の状態に戻されます。
  5. 割り込み処理とコンテキスト保存: 割り込みは、ハードウェア障害、外部デバイス要求など、コンピューター システムにおける一般的なイベントです。システムが中断されると、中断イベントを処理するために現在実行中のプログラムを一時停止する必要があります。スタックは割り込み処理において重要な役割を果たし、プログラムの実行コンテキストを保存および復元するために使用されます。割り込みが発生すると、システムは現在のプログラムの実行サイト (プログラム カウンタやレジスタなどのステータス情報を含む) をスタックに自動的に保存します。次に、割り込みサービス ルーチン (割り込みサービス ルーチン、ISR) が実行され、割り込みイベントが処理されます。処理が完了すると、システムは以前に保存した実行サイトをスタックから復元し、中断されたプログラムの実行を継続します。スタックの保存および復元操作を通じて、割り込み処理フローが正しく、元のプログラムの実行が破壊されないことが保証されます。

スタックの基本動作

  1. スタックを初期化します。
  2. プッシュして要素をスタックに追加します。
  3. ポップ、スタックの一番上から要素を削除します。
  4. スタックの最上位要素を取得します。
  5. スタックが空かいっぱいかを判断します。
  6. スタックの破壊。

知らせ: スタックを操作するときは、「オーバーフロー」と「アンダーフロー

C言語

スタックを実装するには 2 つの方法があり、1 つは配列を使用して実装され、もう 1 つはリンク リストを使用して実装されます。以下に、配列とリンク リストを実装する利点と欠点をまとめます。

配列を使用する利点
1. 配列はメモリ内に連続して格納されるため、要素へのアクセスが高速になります。CPU キャッシュ ヒット率が高くなります。
2. 添字へのランダムアクセス。末尾の挿入と末尾の削除の効率が良い
3. 配列の実装は比較的単純で、要素間の関係を維持するために追加のポインタは必要ありません。
配列実装のデメリット
1. 配列のサイズは固定されているため、スタック領域が不足した場合は拡張する必要があり、パフォーマンスの低下につながる可能性があります。
2. 要素を削除する場合、配列内の他の要素を移動する必要があるため、パフォーマンスの低下を引き起こす可能性があります。

リンク リストを使用する利点
1. リンク リストのサイズは動的に調整できるため、スペースをより有効に活用できます。
2. 任意の位置への挿入と削除は O(1) であり、他の要素を移動する必要がないため、リンク リストのパフォーマンスが高くなります。
リンクリスト実装のデメリット
1. CPUのキャッシュヒット率が低くなり、継続的に格納されないため要素へのアクセス速度が遅くなります。
2. 添字へのランダム アクセスはサポートされていません。

リンク リストと配列構造のどちらを使用するかは、特定のアプリケーション シナリオと要件によって異なります。以下に、配列スタックとリンク リスト スタックの C 言語実装を示します。

配列スタック

#include <stdio.h>
#include <stdlib.h>
#define MAX_SIZE 100

// 定义栈结构
typedef struct {
    int data[MAX_SIZE]; // 用数组存储栈的元素
    int top; // 栈顶指针
} Stack;

// 初始化栈
void init(Stack *stack) {
    stack->top = -1; // 初始化栈顶指针为-1,表示栈为空
}

// 判断栈是否为空
int isEmpty(Stack *stack) {
    return stack->top == -1;
}

// 判断栈是否已满
int isFull(Stack *stack) {
    return stack->top == MAX_SIZE - 1;
}

// 入栈操作
void push(Stack *stack, int item) {
    if (isFull(stack)) {
        printf("Stack overflow\n");
        return;
    }
    stack->top++; // 栈顶指针加1
    stack->data[stack->top] = item; // 将元素入栈
}

// 出栈操作
int pop(Stack *stack) {
    int item;
    if (isEmpty(stack)) {
        printf("Stack underflow\n");
        return -1;
    }
    item = stack->data[stack->top]; // 获取栈顶元素
    stack->top--; // 栈顶指针减1
    return item;
}

// 获取栈顶元素
int peek(Stack *stack) {
    if (isEmpty(stack)) {
        printf("Stack is empty\n");
        return -1;
    }
    return stack->data[stack->top];
}

// 销毁栈
void destroy(Stack *stack) {
    stack->top = -1; // 将栈顶指针重置为-1,表示栈为空
}

int main() {
    Stack stack;
    init(&stack);

    push(&stack, 1);
    push(&stack, 2);
    push(&stack, 3);

    printf("Top element: %d\n", peek(&stack));

    printf("Popped element: %d\n", pop(&stack));
    printf("Popped element: %d\n", pop(&stack));

    printf("Top element: %d\n", peek(&stack));

    destroy(&stack);

    return 0;
}

リンクリストスタック

#include <stdio.h>
#include <stdlib.h>

// 定义链表节点
typedef struct Node {
    int data;
    struct Node* next;
} Node;

// 定义栈结构
typedef struct {
    Node* top; // 栈顶指针
} Stack;

// 初始化栈
void init(Stack* stack) {
    stack->top = NULL; // 初始化栈顶指针为空
}

// 判断栈是否为空
int isEmpty(Stack* stack) {
    return stack->top == NULL;
}

/* 由于链表实现的栈理论上没有大小限制,因此不存在“栈满”的情况。在入栈操作时只需要创建新节点,并将其插入到链表头部即可。
如需限制栈的大小,可以通过设置一个变量来记录当前栈中存储的元素个数,然后在入栈时进行判断,若已满则不允许再次入栈。*/

// 入栈操作
void push(Stack* stack, int item) {
    Node* newNode = (Node*)malloc(sizeof(Node)); // 创建新节点
    if (newNode == NULL) {
        printf("Memory allocation failed\n");
        return;
    }
    newNode->data = item; // 设置新节点的数据为要入栈的元素
    newNode->next = stack->top; // 将新节点插入到栈顶
    stack->top = newNode; // 更新栈顶指针
}

// 出栈操作
int pop(Stack* stack) {
    if (isEmpty(stack)) {
        printf("Stack underflow\n");
        return -1;
    }
    Node* topNode = stack->top; // 获取栈顶节点
    int item = topNode->data; // 获取栈顶元素
    stack->top = topNode->next; // 更新栈顶指针
    free(topNode); // 释放栈顶节点的内存
    return item;
}

// 获取栈顶元素
int peek(Stack* stack) {
    if (isEmpty(stack)) {
        printf("Stack is empty\n");
        return -1;
    }
    return stack->top->data;
}

// 销毁栈
void destroy(Stack* stack) {
    while (!isEmpty(stack)) {
        pop(stack);
    }
}

int main() {
    Stack stack;
    init(&stack);

    push(&stack, 1);
    push(&stack, 2);
    push(&stack, 3);

    printf("Top element: %d\n", peek(&stack));

    printf("Popped element: %d\n", pop(&stack));
    printf("Popped element: %d\n", pop(&stack));

    printf("Top element: %d\n", peek(&stack));

    destroy(&stack);

    return 0;
}

キューはスタックの兄弟構造であり、一方の端で要素を挿入し、もう一方の端で要素を削除することのみを許可する線形データ構造です。挿入操作の終わりはキューの末尾と呼ばれ、削除操作の終わりはキューの先頭と呼ばれます。キュー内のデータ要素はFIFO (先入れ先出し) の原則に従います。つまり、最初に入力された要素が最初にアクセスされます。

画像の説明を追加してください

キューの特徴

  1. 先入れ先出し (FIFO): 挿入された最初の要素が削除される最初の要素となるため、先入れ先出しの順序として表示されます。
  2. 要素は、キューの最後から (キューに) 挿入し、キューの先頭から (キューから) 削除することのみ可能です。

キューアプリケーション

  1. メッセージ パッシング: メッセージ パッシング モデルでは、メッセージはキューに送信され、受信者によって処理されます。送信者はエンキュー操作を通じてキューにメッセージを送信でき、受信者はデキュー操作を通じてキューからメッセージを取得します。
  2. バッファ領域管理: ネットワーク通信やディスク I/O などのシナリオでは、データ バッファを管理するためにキューが使用されます。受信したデータはキューに入れられ、その後、データが順番に送信されることを保証するために、特定のルールに従ってキューから取り出されます。
  3. タスク スケジューリング: オペレーティング システムのタスク スケジューリングでは、通常、キューを使用して実行するタスクを管理し、先着順サービス (FCFS) の原則に従ってスケジュールを設定します。
  4. 幅優先検索: グラフの幅優先検索アルゴリズム (BFS) は、キューを使用してアクセスするノードを保持します。開始ノードから始めて、それをキューに入れ、キューが空になるまでキューからノードを取り出し、隣接するノードをキューに入れ続けます。

キューの基本操作

  1. エンキュー: キューの最後に要素を挿入します。
  2. デキュー: キューの先頭から要素を削除して戻ります。
  3. キューの先頭にある要素の値を取得します。
  4. キュー内の要素の数を取得します。
  5. キューが空かいっぱいかを判断します。
  6. キューの破壊

C言語

キューを実装するには 2 つの方法があり、1 つは配列を使用して実装され、もう 1 つはリンク リストを使用して実装されます。以下に、配列とリンク リストを実装する利点と欠点をまとめます。

配列を使用する利点
1. 配列はメモリ内に連続して格納されるため、要素へのアクセスが高速になります。CPU キャッシュ ヒット率が高くなります。
2. 配列の実装は比較的単純で、要素間の関係を維持するために追加のポインターは必要ありません。
配列実装のデメリット
1. キューの最大長を事前に決める必要があるため、パフォーマンスが低下する可能性があります。
2. キューの順序を維持するには、要素を移動する必要があります。

リンクリストを使用するメリット
1. キューの最大長を事前に決める必要がなく、動的にキューを拡張できます。
2. 挿入および削除操作ではポインタを変更するだけでよく、要素を移動する必要はありません。
3. 複数のキューがリンク リストを共有できます。
リンクリスト実装のデメリット
1. CPUのキャッシュヒット率が低くなり、継続的に格納されないため要素へのアクセス速度が遅くなります。
2. 実装は比較的複雑です。

リンク リストを使用するか配列構造を使用するかにかかわらず、この質問に対する答えは、特定のアプリケーション シナリオと要件によって異なります。以下に、配列キューとリンク リスト キューの C 言語実装を示します。

アレイキュー

#include <stdio.h>
#include <stdlib.h>

#define MAX_SIZE 100 // 队列的最大大小

// 定义队列结构体
struct queue {
    int* arr; // 数组指针
    int front; // 队首位置
    int rear; // 队尾位置
    int size; // 当前队列中存储的元素个数
};

// 初始化队列
struct queue* init() {
    struct queue* q = (struct queue*)malloc(sizeof(struct queue));
    q->arr = (int*)malloc(MAX_SIZE * sizeof(int));
    q->front = 0;
    q->rear = -1;
    q->size = 0;
    return q;
}

// 判断队列是否为空
int is_empty(struct queue* q) {
    return q->size == 0;
}

// 判断队列是否已满
int is_full(struct queue* q) {
    return q->size == MAX_SIZE;
}

// 入队
void enqueue(struct queue* q, int value) {
    if (is_full(q)) {
        printf("Queue Overflow\n");
        return;
    }
    q->rear = (q->rear + 1) % MAX_SIZE;
    q->arr[q->rear] = value;
    q->size++;
}

// 出队
int dequeue(struct queue* q) {
    if (is_empty(q)) {
        printf("Queue Underflow\n");
        return -1;
    }
    int value = q->arr[q->front];
    q->front = (q->front + 1) % MAX_SIZE;
    q->size--;
    return value;
}

// 获取队首元素
int front(struct queue* q) {
    if (is_empty(q)) {
        printf("Queue Underflow\n");
        return -1;
    }
    return q->arr[q->front];
}

// 获取队列长度
int size(struct queue* q) {
    return q->size;
}

// 销毁队列
void destroy(struct queue* q) {
    free(q->arr);
    free(q);
}

int main() {
    struct queue* q = init();
    enqueue(q, 10);
    enqueue(q, 20);
    enqueue(q, 30);
    printf("%d\n", dequeue(q)); // 输出10
    printf("%d\n", front(q)); // 输出20
    enqueue(q, 40);
    printf("%d\n", dequeue(q)); // 输出20
    printf("%d\n", dequeue(q)); // 输出30
    printf("%d\n", dequeue(q)); // 输出40
    printf("%d\n", dequeue(q)); // 输出Queue Underflow
    destroy(q); // 销毁队列
    return 0;
}

リンクリストキュー

#include <stdio.h>
#include <stdlib.h>

// 定义队列节点结构体
struct queue_node {
    int data;
    struct queue_node* next;
};

// 定义队列结构体
struct queue {
    struct queue_node* front; // 队首指针
    struct queue_node* rear; // 队尾指针
    int size; // 当前队列中存储的元素个数
};

// 初始化队列
struct queue* init() {
    struct queue* q = (struct queue*)malloc(sizeof(struct queue));
    q->front = NULL;
    q->rear = NULL;
    q->size = 0;
    return q;
}

// 判断队列是否为空
int is_empty(struct queue* q) {
    return q->size == 0;
}

// 同链表栈,链表队列没有固定的大小限制,因此不需要判断队列是否已满

// 入队
void enqueue(struct queue* q, int value) {
    // 创建新节点
    struct queue_node* new_node = (struct queue_node*)malloc(sizeof(struct queue_node));
    new_node->data = value;
    new_node->next = NULL;

    if (is_empty(q)) {
        q->front = new_node;
        q->rear = new_node;
    } else {
        q->rear->next = new_node;
        q->rear = new_node;
    }

    q->size++;
}

// 出队
int dequeue(struct queue* q) {
    if (is_empty(q)) {
        printf("Queue Underflow\n");
        return -1;
    }

    struct queue_node* temp = q->front;
    int value = temp->data;

    if (q->front == q->rear) {
        q->front = NULL;
        q->rear = NULL;
    } else {
        q->front = q->front->next;
    }

    free(temp);
    q->size--;

    return value;
}

// 获取队首元素
int front(struct queue* q) {
    if (is_empty(q)) {
        printf("Queue Underflow\n");
        return -1;
    }
    return q->front->data;
}

// 获取队列长度
int size(struct queue* q) {
    return q->size;
}

// 销毁队列
void destroy(struct queue* q) {
    while (!is_empty(q)) {
        dequeue(q);
    }
    free(q);
}

int main() {
    struct queue* q = init();
    enqueue(q, 10);
    enqueue(q, 20);
    enqueue(q, 30);
    printf("%d\n", dequeue(q)); // 输出10
    printf("%d\n", front(q)); // 输出20
    enqueue(q, 40);
    printf("%d\n", dequeue(q)); // 输出20
    printf("%d\n", dequeue(q)); // 输出30
    printf("%d\n", dequeue(q)); // 输出40
    printf("%d\n", dequeue(q)); // 输出Queue Underflow
    destroy(q); // 销毁队列
    return 0;
}

結論は

スタックとキューは一般的なデータ構造として、アルゴリズムとプログラミングにおいて重要な役割を果たします。この記事では、スタックとキューの特性、アプリケーション シナリオ、C 言語での実装についてまとめます。その原理と応用を深く理解することで、問題をより適切に解決し、アルゴリズムを最適化することができます。この記事が読者のスタックとキューの学習と適用に役立つことを願っています。

おすすめ

転載: blog.csdn.net/a2360051431/article/details/130978787