前言
資料結構,一門資料處理的藝術,精巧的結構在一個又一個演算法下發揮著他們無與倫比的高效和精密之美,在為資訊技術打下堅實地基的同時,也令無數開發者和探索者為之著迷,
也因如此,它作為博主大二上學期最重要的必修課出現了,由于大家對于上學期C++系列博文的支持,我打算將這門課的筆記也寫作系列博文,既用于整理、消化,也用于同各位交流、展示資料結構的美,
此系列文章,將會分成兩條主線,一條“資料結構基礎”,一條“資料結構拓展”,“資料結構基礎”主要以記錄課上內容為主,“拓展”則是以課上內容為基礎的更加高深的資料結構或相關應用知識,
歡迎關注博主,一起交流、學習、進步,往期的文章將會放在文末,
前面總結了線性表的基本使用,這一節我們來總結一下兩種常用的也是非常重要且特殊的線性結構——堆疊和佇列,
說它重要,是因為這種結構在日常生產中的大大小小事務中無時無刻不在用到,
例如程式呼叫函式時產生的存盤就是堆疊結構,所以我們常說“堆疊空間”,先呼叫的函式最后結束,后來函式內的區域變數的空間都會在該函式結束時得到釋放,且不會影響到前面的函式,而計算機底層指令執行就是一種佇列結構,后來的指令最后執行,
不僅如此,在各種演算法中,兩個資料結構也是頻頻入鏡,以最基本的兩種搜索演算法為例,深度優先和廣度優先就是基于這兩種結構對狀態進行的列舉,
另外,面對事務并發的場景,一種不錯的解決方案是使用佇列管理事務,將其佇列化后依次處理,常見的輸入輸出流也是位元組資料的佇列,面向物件中一個類實體化和釋放時他本身及其繼承的父類關系就是堆疊的關系,
所以,這兩種基礎資料結構的重要性是不言而喻的,俗話說“基礎不牢,地動山搖”,要建立堅實可靠演算法基礎和業務邏輯能力,本節基礎內容和下一節的應用擴展內容將會是一個繞不開的重點,那么就讓我們開始吧:
本節思維導圖:

堆疊
堆疊簡稱堆疊,是一種首先得線性表,只允許表的同一段進行插入和洗掉操作,且這些操作是按后勁消除的原則進行的,
進行插入和洗掉的一段被稱為堆疊頂,另一端被稱為堆疊底,堆疊中沒有元素的時候被稱為空堆疊
順序堆疊
順序堆疊的存盤方式是順序的,使用陣列實作,
其特點就是堆疊的最大規模是確定的,因為陣列的大小無法動態改變,
順序堆疊的封裝
下面我們來封裝一個順序堆疊,最基本操作如下:
- 壓入一個元素(push)
- 彈出一個元素(pop)
- 獲得堆疊頂元素(peek)
- 清空堆疊(clear)
- 判斷是否為空(isEmpty)
- 判斷是否存滿(isFull)
- 獲得元素個數(size)
在順序堆疊中,我們只需要知道一個陣列的地址、當前元素個數以及陣列規模便可以完成堆疊的基本操作,
所以堆疊的類的原型以及建構式可如下(以int元素為例):
//C
typedef struct _Stack{
int * elements;
int maxLen;
int size;
}Stack;
Stack * createStack(int maxLen){
Stack * stack = (Stack *)malloc(sizeof(Stack));
stack->maxLen = max(1,maxLen);
stack->size = 0;
stack->stack = (int *)malloc(sizeof(int) * min(1,maxLen));
return stack;
}
//java
public class Stack {
private int[] elements;
private int maxLen;
private int size;
public Stack(int maxLen) {
this.maxLen = Math.max(1,maxLen);
elements= new int[maxLen];
size = 0;
}
}
判斷是否空/滿
有了這樣的原型,判斷一個堆疊是否慷訓者滿就很簡單了,
空就是當前規模為0,滿就是當前規模和最大規模相同,
//C
int isEmpty(Stack * stack){
return stack->size == 0;
}
int isFull(Stack * stack){
return stack->size == stack->maxLen;
}
public boolean isEmpty() {
return size == 0;
}
public boolean isFull() {
return size == maxLen;
}
獲得當前堆疊中元素數量
在確定了堆疊的原型之后,這個問題就變成了一個送分題,因為堆疊中元素數量就在里面存著,
//C
int size(Stack * stack){
return stack->size;
}
public int size() {
return size;
}
壓入一個元素
在堆疊還有剩余空間的條件下,插入一個元素只需要將該元素放置在陣列末尾 ,同時更新堆疊頂位置即可,
所以在執行插入操作之前,我們需要先對堆疊是否已滿進行判斷
//C
int push(int value,Stack * stack){
if(isFull(stack)){
return 0; // 插入元素失敗,堆疊滿
}
Stack->elements[stack->size] = value;
stack->size++;
return 1;
}
//java
public boolean push(int value) throws Exception {
if(isFull()) {
throw new Exception("堆疊滿不能添加元素");
}
elements[size] = value;
size++;
return true;
}
獲得堆疊頂元素
獲得堆疊頂元素,堆疊頂元素就是當前陣列中的最后元素,直接按照下標回傳即可,
但在這之前需要判定一下堆疊是否為空,
//C
int peek(Stack * stack){
if(isEmpty(stack)){
return 0;
}
return stack->elements[stack->size - 1];
}
public int peek() throws Exception {
if(isEmpty()) {
throw new Exception("空堆疊例外");
}
return elements[size - 1];
}
彈出堆疊頂元素
當堆疊頂元素存在時,順序堆疊只需要將堆疊頂指標向前移動一位即可表示洗掉,對于空堆疊,可以選擇拋出例外,也可以不做任何處理,
//C
int pop(Stack * stack){
stack->size = max(0,stack->size - 1);
return stack->size;
}
public int pop() {
size = Math.max(0, size - 1);
return size;
}
清空堆疊
清空堆疊相當于洗掉所有的元素,只需要將堆疊頂指標置零即可,
//C
int clear(Stack * stack){
int size = stack->size;
stack->size = 0;
return size;
}
//java
public int clear() {
int size = this.size;
this.size = 0;
return size;
}
完整代碼
//C
#include<malloc>
typedef struct _Stack{
int * elements;
int maxLen;
int size;
}Stack;
Stack * createStack(int);
int isEmpty();
int isFull();
int push(Stack*,int);
int pop(Stack*);
int peek(Stack*);
int size(Stack*);
int clear(Stack*);
Stack * createStack(int maxLen){
Stack * stack = (Stack *)malloc(sizeof(Stack));
stack->maxLen = max(1,maxLen);
stack->size = 0;
stack->stack = (int *)malloc(sizeof(int) * min(1,maxLen));
return stack;
}
int isEmpty(Stack * stack){
return stack->size == 0;
}
int isFull(Stack * stack){
return stack->size == stack->maxLen;
}
int push(int value,Stack * stack){
if(isFull(stack)){
return 0; // 插入元素失敗,堆疊滿
}
Stack->elements[stack->size++] = value;
return 1;
}
int peek(Stack * stack){
if(isEmpty(stack)){
return 0;
}
return stack->elements[stack->size - 1];
}
int pop(Stack * stack){
stack->size = max(0,stack->size - 1);
return stack->size;
}
int size(Stack * stack){
return stack->size;
}
int clear(Stack * stack){
int size = stack->size;
stack->size = 0;
return size;
}
//java
public class Stack {
private int[] elements;
private int maxLen;
private int size;
public Stack(int maxLen) {
this.maxLen = Math.max(1,maxLen);
elements = new int[maxLen];
size = 0;
}
public boolean isEmpty() {
return size == 0;
}
public boolean isFull() {
return size == maxLen;
}
public boolean push(int value) throws Exception {
if(isFull()) {
throw new Exception("堆疊滿不能添加元素");
}
elements[size] = value;
size++;
return true;
}
public int pop() {
size = Math.max(0, size - 1);
return size;
}
public int peek() throws Exception {
if(isEmpty()) {
throw new Exception("空堆疊無堆疊頂");
}
return elements[size - 1];
}
public int size() {
return size;
}
public int clear() {
int size = this.size;
this.size = 0;
return size;
}
}
鏈式堆疊
鏈式堆疊是指使用鏈式存盤結構實作的堆疊,載體就是鏈表,
鏈式堆疊由于其使用鏈表實作,所以其相對于順序堆疊來說沒有了最大元素個數的上限,因此鏈式堆疊沒有了判斷堆疊滿的方法,因為鏈式堆疊理論上是無限的,
相較于順序堆疊來說,鏈式堆疊的一些操作會有些不同,最突出的是壓堆疊和彈堆疊,原因在于順序堆疊的堆疊頂指標并不是實際的元素指標,而是模擬的堆疊頂元素下標,再洗掉元素的時候是直接將堆疊頂指標減一,堆疊頂元素自然就被排除在了堆疊之外,而并沒有被真正洗掉,但是在鏈式堆疊這里,洗掉操作除了要將堆疊頂元素提出堆疊之外,還要切實的釋放掉它,
另外,鏈式堆疊的封裝和順序堆疊也不一樣,不再需要保留全部的陣列而是僅保留一個頭指標即可,這樣大大減少了封裝物件的大小,
最后一個問題,就是有關鏈式堆疊的堆疊頂在哪邊的問題,是放在鏈首好還是放在鏈尾好,其實這個問題非常好回答以至于基本上就是顯而易見的,由于堆疊結構的特殊性,我們需要頻繁的訪問堆疊頂或插入洗掉堆疊頂,而鏈表中唯一可以直接訪問到的元素就是鏈首,要是訪問鏈尾則需要每次遍歷整個堆疊,這個復雜度是我們不可接受的,
需要注意的是,鏈式堆疊中沒有哨位結點,而且基本操作也比單鏈表簡單
(當然者不代表使用哨位結點不行,只是實作起來會麻煩一些)
鏈式堆疊的封裝
在封裝具體的方法之前,先來封裝下鏈式堆疊物件和結構體,
//C
typedef struct _Node{
int value;
struct Node * next;
}Node;
typedef struct _LinkedStack{
Node * head;
int size;
}LinkedStack;
LinkedStack * createLinkedStack(){
LinkedStack stack = (LinkedStack*)malloc(sizeof(LinkedStack));
stack->head = NULL;
stack->size = 0;
}
//java
public class LinkedStack {
private class Node{
int value;
Node next;
}
private Node head;
private int size;
public LinkedStack() {
size = 0;
head = null;
}
}
有了鏈表結構體和類宣告,下面就可以封裝對應的方法了,
當然,我們沒必要封裝所有的方法,只需要封裝幾個有明顯不同實作的方法即可,
壓堆疊
插入一個元素,需要新建一個結點并將它加入隊首,
//C
int push(LinkedStack * stack,int value){
Node * node = (Node*)malloc(sizeof(Node));//新建一個結點
/*設定結點資訊*/
node->value = value;
node->next = stack->head;
/*插入結點*/
stack->head = node;
stack->size++;
return 1;
}
//java
public boolean push(int val) {
Node node = new Node();
node.value = val;
node.next = head;
head = node;
return true;
}
彈堆疊
彈出堆疊頂元素需要將堆疊頂元素踢出鏈表并釋放空間
//C
int pop(LinkedStack * stack){
if(isEmpty()){
return false;
}
Node * node = stack->head;//快取一下不然會出事
stack->head = node->next;//踢出鏈表
free(node);//釋放空間
return 1;
}
public boolean pop() {
if(isEmpty()) {
return false;
}
/*注:java自帶垃圾回識訓制,不需要手動釋放*/
head = head.next;
return true;
}
獲取堆疊頂元素
//C
int peek(LinkedStack * stack){
if(isEmpty(stack)){
return 0;
}
return stack->head->value;
}
public int peek() throws Exception {
if(isEmpty()) {
throw new Exception("空堆疊例外");
}
return head.value;
}
清空堆疊
清空堆疊的難點主要體現在將堆疊中的結點逐個釋放掉
//C
int clear(LinkedStack * stack){
Node * node;
while(stack->head){
node = stack->head;
stack->head = node->next;
free(node);
}
size = 0;
return 1;
}
//java
public boolean clear() {
size = 0;
head = null;
//由于java存在垃圾回識訓制,所以不用手動的釋放結點記憶體
return true;
}
佇列
顧名思義,佇列在使用起來時就像現實生活中排隊的佇列一般,
佇列也是一種受限的線性結構,插入操作只能將元素放在一端而查詢和洗掉操作只能對另一端使用,
這樣的限制導致了佇列元素特殊的先進先出特性,

這么看,是不是挺像排隊的俯視圖的,
既然是特殊的線性結構,那就會有順序的和鏈接的兩種存盤模式,他們分別對應著順序佇列,和鏈式佇列,下面我們就來介紹并封裝他們,
順序佇列
順序佇列是指佇列元素在存盤空間上是順序的,它采用順序表也就是陣列存盤,
在陣列中表示佇列,需要隊首和隊尾兩個指標,

當需要插入一個元素時,僅需要將新的元素放置在隊尾,并將隊尾指標向后移動一格,
當需要彈出元素時,僅需要將隊首指標向后移動一格即可,
因為隊首和隊尾每次都是只增不減,所以整個佇列看起來就像是在朝著隊尾方向前進,

那么空佇列也就是佇列中沒有元素時,其標志就是隊首位于隊尾的右面一格,

這樣一來,當加入一格新元素的時候,隊尾就會和隊首重合,這個唯一的元素就既是隊首也是隊尾,

當洗掉最后一個元素時,隊首會向前移動,位于隊尾的后方,回到空佇列的情況,
但是這樣有一個明顯的弊端,那就是佇列的插入次數是有限的,因為陣列的大小總是有上限的,每次插入都會使佇列向前移動一步,而每次彈出的空間卻不能在被佇列使用,這樣就造成了極大地不便,也是我們不能接受的,我們所希望的,是佇列的上限取決于佇列的長度,而非歷史插入次數,
回圈佇列
于是我們便引出回圈佇列來解決這個問題,回圈佇列的思路就是將陣列首位相接,允許隊首和隊尾指標越過陣列的邊界再回到開始位置,這個操作可以使用取模運算來完成,

只是這樣的設計存在一個問題,那就是不能再像之前那樣根據隊首和隊尾的位置來判斷佇列是否為空了,隊尾指標出現在隊首指標后面時,還有可能是整個陣列都是佇列,且當佇列翻越陣列終點時,隊尾指標也會在隊首指標后面,


所以必須使用指標之外的辦法來管理陣列,
一個很直接的方法就是引入佇列中元素個數和陣列長度兩個屬性,這樣一來只需要在每次插入和洗掉的時候維護一下元素個數即可,就可以根據元素個數及陣列長度來判斷佇列是否慷訓者滿,
順序佇列的封裝
封裝一個順序佇列,至少要完成一下的基礎操作:
- 入隊:向隊尾添加元素
- 出隊:洗掉隊首元素
- 獲取隊首元素
- 判斷佇列是否為滿
- 判斷佇列是否為空
順序佇列的類
在封裝基礎操作之前,不妨先來給順序佇列的結構體和類封裝一下,這是所有操作的基礎,
經過上面的分析,實作順序佇列,就是用回圈佇列實作,所以類成員至少需要包括以下三個資訊:
- 佇列陣列
elements - 陣列規模
maxLen - 佇列規模
size - 隊首位置
head - 隊尾位置
tail
//C
typedef struct _ArrayQueue{
int * elements;
int maxLen;
int size;
int head;
int tail;
}ArrayQueue;
ArrayQueue * createArrayQueue(int maxLen){
ArrayQueue * queue = (ArrayQueue*)malloc(sizeof(ArrayQueue));
maxLen = min(1,maxLen);
queue->elements = (int*)malloc(sizeof(int) * maxLen);
queue->maxLen = maxLen;
queue->size = 0;
queue->head = 0;
queue->tail = maxLen - 1;
}
public class ArrayQueue {
private int[] elements;
private int maxLen;
private int size;
public ArrayQueue(int maxLen) {
maxLen = Math.min(1, maxLen);
elements = new int[maxLen];
this.maxLen = maxLen;
size = 0;
head = 0;
tail = maxLen - 1;
}
}
獲取佇列規模
獲取佇列規模只需要回傳其中存盤的size即可,
int size(ArrayQueue * queue){
if(queue == NULL)
return 0;
return queue->size;
}
//java
public int size() {
return size;
}
判斷佇列是否為空
當佇列當前元素數量為0時佇列為空
//C
int isEmpty(ArrayQueue * queue){
if(queue == NULL)
return 0;
return queue->size == 0;
}
//java
public boolean isEmpty() {
return size == 0;
}
判斷佇列是否為滿
當佇列中元素個數與最大元素個數相等時,佇列為滿
//C
int isFull(ArrayQueue * queue){
if(queue == NULL)
return 0;
return queue->size == queue->maxLen;
}
//java
public boolean isFull() {
return size == maxLen;
}
入隊
入隊需要首先判斷佇列是否為滿,如果沒有滿則可以入隊,將新元素放到隊尾,同時維護隊尾指標和佇列規模,
//C
int push(ArrayQueue * queue,int element){
if(isFull(queue))
return 0;
queue->tail = (queue->tail + 1) % queue->maxLen;
queue->elements[tail] = element;
queue->size++;
return 1;
}
//java
public boolean push(int element) {
if(isFull()){
return false;
}
tail = (tail + 1) % maxLen;
elements[tail] = element;
size++;
return true;
}
出隊
出隊之前需要先判斷佇列是否為空,若不為空則直接將頭指標向后移動一位同時維護佇列長度即可,
//C
int pop(ArrayQueue * queue){
if(isEmpty(queue))
return 0;
queue->head = (queue->head + 1) % queue->maxLen;
queue->size--;
return 1;
}
//java
public boolean pop() {
if(isEmpty())
return false;
head = (head + 1) % maxLen;
size--;
return true;
}
隊首元素
//C
int front(ArrayQueue * queue){
if(isEmpty())
return 0;
return queue->elements[queue->head];
}
//java
public int front() throws Exception {
if(isEmpty()) {
throw new Exception("空佇列例外");
}
return elements[head];
}
清空佇列
清空佇列相當于將佇列恢復出廠設定,只需要將頭尾指標歸為并將元素個數置零即可,
//C
int clear(ArrayQueue * queue){
queue->head = 0;
queue->tail = -1;
queue->size = 0;
return 1;
}
//java
public boolean clear() {
size = 0;
head = 0;
tail = -1;
return true;
}
完整代碼
//C
typedef struct _ArrayQueue{
int * elements;
int maxLen;
int size;
int head;
int tail;
}ArrayQueue;
ArrayQueue * createArrayQueue(int maxLen){
ArrayQueue * queue = (ArrayQueue*)malloc(sizeof(ArrayQueue));
maxLen = min(1,maxLen);
queue->elements = (int*)malloc(sizeof(int) * maxLen);
queue->maxLen = maxLen;
queue->size = 0;
queue->head = 0;
queue->tail = maxLen - 1;
}
int size(ArrayQueue * queue){
if(queue == NULL)
return 0;
return queue->size;
}
int isEmpty(ArrayQueue * queue){
if(queue == NULL)
return 0;
return queue->size == 0;
}
int isFull(ArrayQueue * queue){
if(queue == NULL)
return 0;
return queue->size == queue->maxLen;
}
int push(ArrayQueue * queue,int element){
if(isFull(queue))
return 0;
queue->tail = (queue->tail + 1) % queue->maxLen;
queue->elements[tail] = element;
queue->size++;
return 1;
}
int pop(ArrayQueue * queue){
if(isEmpty(queue))
return 0;
queue->head = (queue->head + 1) % queue->maxLen;
queue->size--;
return 1;
}
int front(ArrayQueue * queue){
if(isEmpty())
return 0;
return queue->elements[queue->head];
}
int clear(ArrayQueue * queue){
queue->head = 0;
queue->tail = -1;
queue->size = 0;
return 1;
}
//java
public class ArrayQueue {
private int[] elements;
private int maxLen;
private int size;
private int head;
private int tail;
public ArrayQueue(int maxLen) {
maxLen = Math.min(1, maxLen);
elements = new int[maxLen];
this.maxLen = maxLen;
size = 0;
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
public boolean isFull() {
return size == maxLen;
}
public boolean push(int element) {
if(isFull()){
return false;
}
tail = (tail + 1) % maxLen;
elements[tail] = element;
size++;
return true;
}
public boolean pop() {
if(isEmpty())
return false;
head = (head + 1) % maxLen;
size--;
return true;
}
public int front() throws Exception {
if(isEmpty()) {
throw new Exception("空佇列例外");
}
return elements[head];
}
public boolean clear() {
size = 0;
head = 0;
tail = -1;
return true;
}
}
注:這里僅考慮完成基本的功能,對于更復雜的需求例如執行緒安全等并沒有考慮,
鏈式佇列
鏈式佇列和鏈式堆疊很類似,都是使用鏈表對佇列進行改造,
且仔細觀察不難發現,單鏈表的結構更適合實作佇列,
在鏈首,元素洗掉更加容易,在鏈尾,元素無法直接得到其前驅,插入更加容易,
所以我們將鏈首當做隊首,將鏈尾當做隊尾,
與鏈式堆疊類似,鏈式佇列中也不需要哨位結點,且沒有最大長度限制
當佇列為空時首尾指標都為空,
鏈式佇列的封裝
首先還是先來封裝鏈式佇列的結構體和類,鏈式佇列僅需要記錄頭結點和尾結點的指標以及佇列規模,沒有佇列的最大長度限制,
//C
typedef struct _Node{
int value;
struct Node * next;
}Node;
typedef struct _LinkedQueue{
Node * head;
Node * tail;
int size;
}LinkedQueue;
LinkedQueue * createLinkedQueue(){
LinkedQueue * queue = (LinkedQueue*)malloc(sizeof(LinkedQueue));
queue->size = 0;
queue->head = NULL;
queue->tail = NULL;
return queue;
}
//java
public class LinkedQueue {
private class Node{
int value;
Node next;
}
private Node head;
private Node tail;
private int size;
public LinkedQueue() {
size = 0;
head = null;
tail = null;
}
}
同鏈式堆疊一樣,這里僅封裝不太一樣的方法:
入隊
入隊時需要在隊尾添加元素,同時更新佇列大小,需要注意的是要單獨處理隊中沒有元素的情況,
//C
int push(LinkedQueue * queue,int value){
Node * node = (Node*)malloc(sizeof(Node));
node->value = value;
node->next = NULL;
if(isEmpty(queue)){//當隊中元素唯一時,該元素既是隊首又是隊尾
queue->head = node;
queue->tail = node;
}else{
queue->tail->next = node;
queue->tail = node;
}
queue->size++;
return 1;
}
//java
public boolean push(int value) {
Node node = new Node();
node.value = value;
node.next = null;
if(isEmpty()) {
head = node;
tail = node;
}else {
tail.next = node;
tail = node;
}
size++;
return true;
}
出隊
出隊的時候需要注意一下,當隊中最后一個元素要被踢出時,需要更新尾指標為空,
//C
int pop(LinkedQueue * queue){
if(isEmpty(queue)){
return 0;
}
Node * node = queue->head;
queue->head = node->next;
if(queue->size = 1){
queue->tail = NULL;
}
free(node);
queue->size--;
return 1;
}
//java
public boolean pop() {
if(isEmpty()) {
return false;
}
head = head.next;
if(size == 1) {
tail = null;
}
size--;
return true;
}
清空佇列
同鏈式堆疊相似,清空佇列也需要將佇列中的元素結點釋放,同時維護佇列規模,并將頭尾指標還原為空,
//C
int clear(LinkedQueue * queue){
queue->size = 0;
Node * node;
while(queue->head){
node = queue->head;
queue->head = node;
free(node);
}
queue->tail = NULL;
return 1;
}
//java
public boolean clear() {
head = null;
tail = null;
size = 0;
return true;
}
往期博客
- 【資料結構基礎】資料結構基礎概念
- 【資料結構基礎】線性資料結構——線性表概念 及 陣列的封裝
- 【資料結構基礎】線性資料結構——三種鏈表的總結及封裝
參考資料:
- 《資料結構》(劉大有,楊博等編著)
- 《演算法導論》(托馬斯·科爾曼等編著)
- 《圖解資料結構——使用Java》(胡昭民著)
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/160367.html
標籤:其他
