目次
スタックの出入りプロセス、スタック ポップ シーケンスの正当性、キューの操作と特性を習得することに重点を置きます。
3.1スタック
3.1.1 基本概念
定義:スタックは、一方の端でのみ挿入または削除を許可する線形リストです。
スタックの最上位 Top:テーブルの終端とも呼ばれ、挿入および削除操作が実行される終端
スタックの底部:ヘッダー、固定端、挿入および削除操作が許可されない端とも呼ばれます。
·特徴:操作はスタックの最上位でのみ実行でき、アクセスノードは後入れ先出しの原則に従います。
論理構造:線形テーブルと同じですが、依然として 1 対 1 の関係です
・基本操作:
InitStack(&S): 空のスタックを初期化します。 S
StackEmpty(S): スタックが空かどうかを判断し、スタック S が空の場合は true を返し、それ以外の場合は false を返します。
Push(&S,x): スタックにプッシュします。スタック s がいっぱいでない場合は、x を追加してスタックの新しい先頭にします。
Pop(&S,&x): スタックをポップアウトします。スタック s が空でない場合は、スタックの最上位要素をポップして x を返します。
GetTop(S,&x): スタックの最上位要素を読み取ります。スタック s が空でない場合は、x を使用してスタックの最上位要素を返します。
DestroyStack(&S): スタックを破棄し、スタックによって占められていたストレージ領域を解放します (「&」は参照呼び出しを意味します)
3.1.2 スタックのシーケンシャル記憶構造
シーケンシャル スタック:シーケンシャル ストレージを使用するスタックはシーケンシャル スタックと呼ばれ、一般的な線形テーブルのシーケンシャル ストレージ構造とまったく同じです。連続したアドレスを持つストレージ ユニットのグループは、スタックの最下位からスタックの最上位に要素を順番に格納するために使用され、トップ ポインタはスタックの最上位 (またはスタックの最上位要素の上にある添字アドレス) を指すために使用されます。
特徴:配列をシーケンシャルスタックとして使用するのはシンプルで便利ですが、固定サイズの配列はオーバーフローしやすいです (オーバーフロー: スタックがいっぱいのときにプッシュし続ける、アンダーフロー: スタックが空のときにポップ、アンダーフローは終了条件として使用できます)
・シーケンシャルスタックの実装
1) 初期化: スタックトップポインタ S.top=-1 を初期化します。
2) スタックが空であると判断します。S.top=-1 の場合、スタックは空であり、それ以外の場合は空ではありません。
3) スタックにプッシュします。スタックがいっぱいではない場合、最初にスタックの先頭ポインタが 1 だけインクリメントされ、次に値がスタックの先頭に送信されます。
bool Push (SqStack &S, ElemType x) {
if (S. tope=MaxSize-1) //栈满, 报错
return false;
S.data[++S.top]=x; //指针先加1,再入栈
return true ;
}
4) スタックからポップアウトします。スタックが空ではない場合、最初にスタックの最上位要素の値を取得し、次にスタックの最上位ポインタを 1 だけデクリメントします。
bool Pop (SqStack &S, ElemType &x) {
if(S. top==-1) //栈空,报错
return false;
x=S.data[S.top--]; //先出栈,指针再减1
return true;
}
5) スタックの最上位要素を読み取ります
bool GetTop (SqStack S, ElemType &x) {
if(S. top==-1) //栈空,报错
return false;
x=S .data[S.top]; //x记录栈顶元素
return true ;
}
共有スタック:スタックの底部の相対的に変化しない位置を利用することで、2 つの連続したスタックが一次元配列を共有でき、2 つのスタックの底部がそれぞれ共有空間の両端に設定され、2 つのスタックの上部が共有空間の中央に向かって伸びます。
両スタックのスタックトップポインタはスタックの先頭要素を指しており、top1=-1の場合はスタック1番が空、top2=Maxsizeの場合はスタック2番が空となり、2つのスタックトップポインタが隣接する場合(top2-top1=1)のみスタックがフルと判定されます。1 番スタックがスタックにプッシュされると、top1 は 1 ずつインクリメントされて値が代入され、2 番スタックがスタックにプッシュされると、top2 は 1 減算されて値が代入されますが、スタックがポップされるときはその逆になります。
スタックを共有する目的は、ストレージ スペースをより効率的に使用することであり、2 つのスタックのスペースは相互に調整され、ストレージ スペース全体がいっぱいになった場合にのみオーバーフローが発生します。データアクセスの時間計算量は 0(1) であるため、アクセス効率には影響しません。
3.1.3 スタックの連鎖ストレージ構造
チェーン スタック:チェーン ストレージを使用するスタックはチェーン スタックと呼ばれ、通常は単一のリンク リストによって実装され、すべての操作がテーブルの先頭で実行されるため、チェーン スタックにはヘッド ノードがなく、ヘッド ポインターはスタックの最上位要素を直接指します。
・特徴:チェーンスタックは、複数のスタックがストレージスペースを共有するのに便利で、効率が向上します。スタックのオーバーフローがなく、ノードの削除と挿入に便利です。チェーンスタックの操作はチェーンリストに似ており、ポップアップとスタックインはすべて先頭で実行されます。
・チェーンスタックの基本操作
1) 初期化: 空のスタックを構築し、先頭ポインタを空に設定します。
2)NULL判定:Lhead=NULL?
3) スタックに入れる
4) ポップアウト
5) スタックの最上位要素を取得します。Lhead->data を直接返します。
[シーケンス スタックとチェーン スタック]:シーケンシャル スタックとチェーン スタックの時間計算量は O(1) と同じですが、スペース パフォーマンスの観点から、シーケンシャル スタックは事前に固定長を決定する必要があるため、メモリ空間を浪費する問題が発生する可能性がありますが、その利点は、アクセスするときに位置を特定するのに非常に便利であること、一方、チェーン スタックは各要素にポインタ フィールドが必要であり、メモリのオーバーヘッドも若干増加しますが、スタックの長さに制限はありません。したがって、スタックの使用中に要素の変更が予測できない場合 (小さい場合もあれば非常に大きい場合もあります)、チェーン スタックを使用するのが最善であり、逆に、変更が制御可能な範囲内にある場合は、シーケンシャル スタックを使用するのが最適です。
3.2 キュー
3.2.1 基本概念
定義:キューは、一方の端 (末尾) での挿入と、もう一方の端での削除 (先頭) のみを許可する線形テーブルです (核酸のキューを思い浮かべてください)。つまり、先入れ先出しの線形テーブルです。
· キューの基本操作:
InitQueue(&Q): キューを初期化し、空のキューを構築します Q
QueueEmpty(Q): キューが空であると判断し、キューQが空の場合はtrueを返し、そうでない場合はfalseを返します。
EnQueue(&Q,x): キューに入ります。キュー Q がいっぱいでない場合は、x を追加してキューの新しい末尾にします
DeQueue (&Q,&x): デキューします。キュー Q が空でない場合は、先頭要素を削除して x で戻ります。
GetHead(Q, &x): キューのヘッド要素を読み取り、キュー e が空でない場合は、キューのヘッド要素を x に割り当てます。
[注意]:スタックまたはキューの途中にあるデータを読み取らないでください。
3.2.2 キューの順次保存
定義:キューの順次ストレージとは、キュー内の要素を格納する連続ストレージ ユニットを割り当て、2 つのポインターを設定することを指します。キュー ヘッド ポインターのフロントはキューのヘッド要素を指し、キューのテール ポインターの後部はキューのテール要素の次の位置を指します (教科書によってフロントとリアの定義が異なる場合があります。リアをキューのテール要素にポイントし、フロントをキューのヘッド要素にポイントすることができます)。
・シーケンシャルストレージ記述型:
#define MaxSize 50 //定义队列中元素的最大个数
typedef struct{
ElemType data [MaxSize] ; //存放队列元素
int front, rear; //队头指针和队尾指针
} SqQueue;
· キュー操作:
初期状態 (キューが空): Q. フロント==Q. リア==0
キュー操作の開始: キューがいっぱいではない場合、最初にキューの最後にある要素に値を送信し、次にキューの最後にあるポインタに 1 を追加します。
デキュー操作: キューが空ではない場合、最初にキュー ヘッド要素の値を取得し、次にキュー ヘッド ポインタに 1 を追加します。
【注意】 rear==maxQsizeの場合、trueオーバーフローとfalseオーバーフローが発生します。
循環キュー:前述の true および false のオーバーフロー状況の場合、解決策は、後部がいっぱいのときに最初から開始し、上図の後部ポインタを 0 の位置にポイントすることで、先頭から末尾までのサイクルが論理的に接続されます。つまり、キュー要素を格納するテーブルは論理的にリングとみなされ、循環キューと呼ばれ、循環キューに抽象化することもできます。
循環キューの実現:モジュロ演算を使用して、要素の挿入時に後部ポインターが 0 を指すこと、つまり Q.rear = (Q.rear+1) % MAXQSIZE を認識し、後部ポインターが最後の位置から最初の位置に移動できるようにします。要素を削除するときの前部ポインターにも同じことが当てはまります (Q.front = (Q.front+1) % MAXQSIZE)。
・循環キューが空と満杯:下の図の場合、空のキューと満杯のキューは後=前ですが、このとき空か満かを判断するにはどうすればよいでしょうか?
1) フラグを設定します。リア = フロント、フラグ = 0 の場合、チームは空です。リア = フロント、フラグ = 1 の場合、チームはフルです。
2) チーム内の要素の数を記録するために別の変数を設定します
3) 使用する要素スペースを 1 つ減らします。後部は前部よりも大きいか小さい可能性があるため、次の図の 2 つの状況のように 1 つの位置または円の違いが存在する可能性があるため、後部と前部のサイズの問題を解決するには、満杯のキューの状態は (後部 + 1) % MAXQSIZE=前部であり、空のキューの状態は依然として後部 = 前部です 。
要約すると、キュー内の要素の数は、( rear-front+ MAXQSIZE)% MAXQSIZEとしても計算できます。上記の分析により、循環キューを実現するための基本的な考え方は次のようになります。
・循環キューの基本操作
1) 初期化: キューの先頭と末尾のポインタを初期化します。 Q.rear =Q.front=0
2) チームが空いていると判断する: Q.リア =Q.フロント
3) エンキュー
bool EnQueue (SqQueue &Q,ElemType x) {
if((Q. rear+1) %MaxSize==Q. front) return false; //队满则报错
Q.data[Q. rear]=x;
Q. rear= (Q. rear+1) MaxSize; //队尾指针加1取模
return true;
}
4) デキュー
bool DeQueue (SqQueue &Q, ElemType &X) {
if(Q. rear==Q. front) return false; //队空则报错
x=Q. data[Q. front] ;
Q. front= (Q. front+1)%MaxSize; //队头指针加1取模
return true;
}
3.2.3 キューの連鎖ストレージ
定義:キューのリンクされたストレージはチェーン キューと呼ばれます。これは、キュー ヘッド ポインタとキュー テール ポインタの両方を持つ単一リンク リストです。キュー ヘッド ポインタはキュー ヘッド ノードを指し、キュー テール ポインタは終端ノードを指します。キューが空の場合、前部と後部の両方がヘッド ノードを指します
・チェーンキューの基本操作
1) 初期化: ヘッド ノードを確立し、キュー ヘッド ポインターを空に設定します。 Q.front->next=NULL
2) エンプティ判定: Q.フロント=Q.リア
3) エンキュー: チェーンの最後に新しいノードを挿入します。
void EnQueue (LinkQueue &Q, ElemType x) {
LinkNode *s= (LinkNode*) malloc (sizeof (LinkNode)) ;
s->data=x; s->next=NULL; //创建新结点,插入到链尾
Q. rear->next=s;
Q. rear=s;
}
4) デキュー: リンクされたリストの最初のノードを削除します。
bool DeQueue (LinkQueue &Q; ElemType &x) {
if (Q. front--Q.rear) return false; //空队
LinkNode *p=Q. front->next;
x=p->data;
Q. front->next=p->next;
if (Q. rear==p)
Q. rear=Q. front; //若原队列中只有一个结点,删除后变空
free(p) ;
return true;
}
3.2.4 両端キュー
定義:両端キューとは、両端がキューに出入りできるキューを指し、その論理構造は線形構造のままです。
出力制限された両端キュー:一方の端では出入りが可能ですが、もう一方の端では両端のエントリ (2 つの入力と 1 つの出力) のみが許可されます。2 つの入力と 1 つの出力を持つ両端のキューの場合、キューに出入りするときに「両側が順番に出入りする」という原則を理解してください。
入力制限された両端キュー:一方の端ではキューの出入りが許可されますが、もう一方の端では両端のキュー (出力 2 つと入力 1 つ) のみが許可されます。出力 2 つと入力 1 つの両端キューの場合は、キューに出入りするときに「順番に入力および出力」の原則を理解します。
3.3 スタックとキューの応用
3.3.1 スタックブラケットの一致の問題
·基本的な考え方:すべての文字を順番にスキャンし、左括弧が見つかったら左括弧をスタックにプッシュし、右括弧に出会ったら一致チェックのために左括弧をスタックの一番上にポップします。このとき、次の 4 つの状況が発生します。
1) マッチングが成功した場合、すべてのブラケットが正常にマッチングされるまで、次のラウンドのマッチングが実行されます。
2) 正常に一致しない括弧がある場合、これらの括弧の一致は不正であり、失敗することを意味します。
3) 右括弧の一致がある場合、スタックは空ですが、このとき、右括弧は 1 つであり、括弧の一致は失敗します。
4) すべての閉じ括弧が処理されると、この時点ではまだスタックに括弧、つまり単一の左括弧が存在し、マッチングも失敗します。
·アルゴリズムの実装:
3.3.2 スタック上の式の評価
・三則演算式
1) 中置式: 演算子は 2 つのオペランド (a+bc*d) の間にあります。
2) 接頭辞式: 演算子は 2 つのオペランドの前にあります (-+ab*cd)
3) 後置式: 演算子は 2 つのオペランド (ab+cd*-) の後に続きます。
・式間の変換
1) インフィックス → サフィックス
①式中の各演算子の演算順序を決定する(演算順序は一意ではなく、対応する式も一意ではない)
② 演算子を順番に選択し、さまざまな式の順番で新しいオペランドを作成します
③ 未処理の演算子が残っている場合は、引き続き②を実行します。
[注]:変換プロセス中は、手動計算と計算の一貫性を確保するために、左優先の原則に従うことが最善です。つまり、左側の演算子が最初に計算できる限り、左側の演算子が最初に計算されます。
スタックの実装: 一時的に演算順序が決定できない演算子を保存するためにスタックを初期化します。各要素を左から右に処理すると、次の 3 つの状況が発生する可能性があります。
① オペランドが見つかりました。接尾辞式を直接結合します。
②区切り文字に遭遇しました。「(」が出現すると、スタックに直接プッシュされます。「)」が出現すると、スタック内の演算子が 1 つずつポップされ、「(」がポップアップされるまで接尾辞式に追加されます。注意: 「(」は接尾辞式に追加されません。
③オペレーターと遭遇。現在の演算子以上の優先順位を持つすべての演算子をスタック内に順番にポップし、後置式に追加します。「(」に遭遇するかスタックが空の場合、演算は停止します。その後、現在の演算子をスタックに置きます。
2) インフィックス → プレフィックス
方法は 1) と同じですが、このときは右優先の原則が適用されます。つまり、右側の演算子が最初に計算できる限り、右側の演算子が最初に計算されます。
・式の計算と実現
1) 接尾辞式: 左から右にスキャンし、演算子が見つかるたびに、演算子の前にある 2 つの最も近いオペランドが対応する演算を実行し、それらを 1 つのオペランドに結合します。
スタックの実装:
① すべての要素が処理されるまで、次の要素を左から右にスキャンします。
②オペランドがスキャンされていればスタックにプッシュして①に戻り、スキャンされていなければ③を実行
③演算子をスキャンするとスタックの先頭にある2つの要素がポップアップされ、対応する演算結果がスタックの先頭に押し戻され、①に戻ります。
2) プレフィックス式: 右から左にスキャンし、演算子が見つかるたびに、演算に続く 2 つのオペランドが対応する演算を実行し、それらを 1 つのオペランドに結合します。
スタック実装: プレフィックス式の場合、①の処理で右から左にスキャンするだけで済みます。
3) 中置式:
スタックの実装: インフィックスをサフィックスに変換し、サフィックス式を使用して評価します
①オペランドスタックと演算子スタックの2つのスタックを初期化する
②オペランドがスキャンされたらオペランドスタックにプッシュする
③演算子または区切り文字がスキャンされると、「中置から接尾辞へ」のロジックに従って演算子スタックにプッシュされます (期間中は演算子もポップアップされ、演算子がポップアップされるたびに、2 つのオペランド スタックの最上位要素がポップアップされて対応する演算が実行され、演算結果がオペランド スタックにプッシュされます)。
3.3.3 スタックと再帰
·関数呼び出し:関数呼び出しのプロセスでは、最後に呼び出された関数が常に最初に実行されます。スタックを使用することで実現されるのは、スタックに最後に格納された関数が最初に呼び出されることです。このプロセスでは、スタックには関数呼び出しの戻りアドレス、実パラメータ、およびローカル変数が格納されます
再帰呼び出し:再帰的に呼び出す場合、関数呼び出しスタックは再帰作業スタックと呼ばれます。再帰の層に入るたびに、再帰呼び出しに必要な情報がスタックの最上部にプッシュされます。再帰の層が終了するたびに、対応する情報がスタックの最上部からポップされます。
短所:効率が低く、再帰の層が多すぎるとスタック オーバーフローが発生します。また、反復計算が大量に含まれる可能性もあります。
3.3.4 キューとレベルのトラバーサル
[バイナリツリーの階層横断]:
[グラフの幅優先走査]: 同じ理由、省略
3.3.5 キューとコンピュータシステム
限られたシステム リソースを複数のプロセスが競合する場合、先着順が一般的な方法であり、この方法はたまたまキューで実装されます。
CPU リソースの割り当て:準備が完了したプロセス キュー、プロセスは先着順で CPU を待ちます
·印刷データバッファ:ここのバッファは印刷するデータを保存するキューを実装でき、ホストとプリンタ間の速度不一致の問題を軽減できます。