超清晰-數據結構之隊列

前言:畢業後忙於工作,較少花時間在基礎上,突然想起某位大佬的名言,當你不知道該學什麼的時候,就去看看基礎,是的,技術的範疇確實太大了,我們很容易就會迷茫,不管是哪門語言的開發者,不管你學的是什麼武功,內功精髓本質上還是一樣的,大學以來,我一直對遞歸和棧的使用處於一種半知半覺的狀態,何爲半知半覺,就是我能說出它的概念以及簡單的應用,但是問細下去,便沒辦法了,這篇文章我想先來說說我對隊列的理解,下一篇再進行棧的學習。

一、隊列

1.隊列的定義:

一種先進先出的數據結構(FIFO)

  隊列可以說是兄弟之間理解起來最爲簡單的一種數據結構了記得我在校園第一次接觸的時候,就直接把它和數組聯繫在一起,因爲本質上,數組就是隊列的一種實現方式,但是我們無法將其等同,很多人會以爲數組是一種數組類型,其實不對,數據類型數據結構是不一樣的,我們可以這麼想,類型是組成結構的一種組成因子,就像我們搭積木一樣,不同積木的堆積方式,就可以搭出不同的模型(結構),結構通過不同的數據類型來實現,數據結構是一種解決數據問題的存在,而且,數組和隊列有一點是不同的,數組完全不是爲隊列而生,只是他的一種特性恰好符合隊列的特性,就是可以隊頭出隊,隊尾進隊,不一樣的是,隊列的概念是不允許你插隊的,而數組本質上是可以的。

 生活中隨處可見隊列的影子,隊列的“隊”,跟生活中的“隊”十分的類似,但是前者不允許插隊,後者你懂得,

2.隊列的存儲:

  實際上,隊列一定是順序存儲的嗎?不,不是的,數組雖然是連續內存存儲的,但是隊列卻不是,你可以想想生活中的兩種隊列,一是排隊買門票,這種隊列一般來說,每個人都是站對位置整齊前進,我們可以說這是一種順序的存儲,把土地按塊劃分成內存塊,每個人都擁有自己的一小塊,而這些小塊加在一起剛好是連續的一大塊,另一種隊列是什麼呢?網上預約啊,每個人在預約平臺預約了自己的位置,都擁有自己的編號,但是大家不需要站在一起,只要用一種標識(預約編號)來識別實際排隊的位置就可以了,其實這就是鏈表實現隊列的原理。

總結來說:隊列有順序表示鏈式表示兩種存儲方式

3.隊列的分類:

 隊列也是分很多種的:

1.單向隊列 :顧明思議,就是隻能允許一端入隊,反向端出隊的隊列

2.雙向隊列:隊列兩端都允許入隊和出隊的操作

3.循環隊列:隊頭和隊尾相連接

在線性表的那篇文章,我有說過順序存儲和鏈式存儲的優缺點,實際上就是一種資源最大利用化的問題,怎麼去最大化的利用現有資源,是每個開發者必須要思考的問題

4.隊列的實現:

初始化一個空隊列時,會向os申請指定長度的內存,此時我們假設這個隊列的長度爲4,那麼我們可以看做這個隊列共有四個方塊組成:

摘一張百科的圖,我們可以看到一開始頭指針和尾指針都是指向 0 的,每次入隊一個元素時,尾指針便+1,每次出隊一個元素時頭指針便加一,我們可以很清楚的看到,在圖c的時候如果在3位置再插入一個D,那麼後面我們將沒有辦法插入新元素,因爲D的後面沒有空間了,但是很明顯0號位明顯是空的,但是按照隊列的要求我們只能從隊尾插入,無法從隊頭插入元素,這就是我們所說的“假溢出”,此時聰明的你可能會發現,將3號位和0號位連在一起形成一個環,那麼0號位不就在3號位後面了,沒錯,這就是循環隊列的想法

  隊列的概念我覺得通過上面的說明你應該大致瞭解了,那麼我們就要說說怎麼實現隊頭出隊,隊尾入隊的要求呢?

3.1 數組(順序存儲)的實現:

  簡單說下數組的概念,數組是順序存儲的一種數據結構,有分爲索引數組和關聯數組(關聯數組PHP用hash map實現),擁有索引和值的屬性,索引是從0開始的,其實在計算機領域很多東西都是從0開始算的,因爲0也是有意義的。在c語言中,數組就是數組,是沒有關聯數組的概念的,所謂的關聯數組,在PHP中,就是索引可以不是數組的數組,類似key-vlaue結構,本質上就是利用的哈希算法,而且大多數強類型的語言的數組空間是一次開闢的,什麼意思呢?就是說數組的長度一開始就是確定的,如果數組滿了,便需要額外擴充,我喜歡PHP的原因之一就是PHP的數組是不需要開發者自己去開闢空間的。而且會根據使用者自動的擴容。好吧扯遠了。

來吧,是時候翻書的代碼了,我們來看看循環隊列的實現:

①隊列的結構體

#define MAXQSIZE 100   //定義一個常量,作爲隊列的最大長度

typedef struct
{
    QElemType *base;   //存儲空間基地址,實際上是一個指針,指向數據存儲的位置

    int front;  //頭指針
    
    int rear;   //尾指針 
}SqQueue;

此時,你可能好奇QElemType是什麼,引百科一段說明來進行解釋:

  ElemType(也有的書上稱之爲elemtp)是數據結構的書上爲了說明問題而用的一個詞。它是element type(“元素的類型”)的簡化體。 因爲數據結構是討論抽象的數據存儲和算法的,一種結構中元素的類型不一定是整型、字符型、浮點型或者用戶自定義類型,爲了不重複說明,使用過程中用“elemtype”代表所有可能的數據類型,簡單明瞭的概括了整體。在算法中,除特別說明外,規定ElemType的默認是int型。

②隊列的初始化

Status InitQueue(SqQueue &Q)
{
  Q.base = new QElemType[MAXQSIZE]; //開闢一個長度爲MAXQSIZE的空間,用於存放數據
  if (!Q.base) { //內存分配失敗時返回的是false
   exit(ERROR);  //程序退出,ERROR是宏定義的字符串,一般是-1
   }
  Q.front = Q.rear = 0;  //頭尾指針指向同一個位置
  return OK;  //返回一個正常標誌,OK一般指1

}

③求隊列的長度

int QueueLength(SqQueue Q)
{
    return (Q.rear-Q.front+MAXQSIZE)%MAXQSIZE; //返回循環隊列的長度
}

如果是普通的隊列,長度則是Q.rear-Q.front

④入隊,將元素填入隊尾,隊尾指針+1

Status EnQueue(SqQueue &Q,QElemType e)
{
  if(Q.rear+1) % MAXQSIZE == Q.front) //這是用來判斷循環隊列是否已滿
    return ERROR;  //ERROR 是 宏定義的一個變量,一般是0
   //隊列不滿
    Q.base[Q.rear]=e;  //e是數據,這句話意思是在索引Q.rear處設置值e,直接把它當數組看就很容易明白呢
    Q.rear = (Q.rear+1)%MAXQSIZE //隊尾指針+1,看成索引+1就好了
    return OK;
}

⑤出隊,隊頭元素出隊,隊頭指針+1

Status DeQueue(SqQueue &Q,QElemType &e)
{
  if (Q.front == Q.rear) return ERROR; //頭指針和尾指針在一起,只有爲空的情況,此時不見得都是指向0
  e=Q.base[Q.front]; //拿出隊頭元素的值
  Q.front = (Q.front+1) % MAXQSIZE; //隊頭元素指針+1,如果只是取隊頭元素此步就不需要了
  return OK;

}

需要注意的是,循環隊列對索引的操作都是取餘的方式,以及Status也是一種泛類型指定,它可以是int 也可以是string,僞代碼的使用十分的頻繁,讀者不需要過多研究

3.2 鏈隊(鏈式存儲)的實現:

其實在之前線性表的文章,我已經很詳細的說明過鏈表了,所以這篇我就不敘述了,如果你對鏈表還不熟悉可以先從去看:https://blog.csdn.net/qq_38378384/article/details/80430396 這篇文章

  其實鏈式存儲方式是一種對內存使用比較良好的存儲方式,它不需要依賴連續的存儲空間,也不需要想順序存儲那樣需要指定一個最大的存儲長度,它的長度是可變的,動態的,這裏我不得不提以下php數組的存儲方式,它就是一種鏈式存儲,雖然它支持for循環和foreach遍歷兩種方式,實際上它的for循環所使用的索引是php所生成的一個映射表,所以在實際情況下,php的for比foreach的效率更加低,一個原因是因爲存在$i的比較,另一個就是需要用到映射表,而foreach則是直接遍歷鏈表,因爲鏈式存儲可以隨意的擴展長度,因此跟強類型的一次性開闢空間的做法相比更有優勢

接下來我們直接來看c語言的鏈隊的僞代碼實現:

①鏈式的存儲結構

typedef struct QNode
{
    QElemType data; //指定類型的數據域
    struct QNode *next;

}QNode,*QueuePtr;


typedef struct
{
    QueuePtr front; //隊頭指針
    QueuePtr rear; //隊尾指針
}LinkQueue;

我們知道 typedef 是 C 語言的一個取別名的關鍵字,這裏大家對結構體不熟悉的話可能會有疑問,這兩個結構體的區別是什麼?

首先,我們不去關心結構體裏面的內容,因爲裏面就是真正要存儲的東西,你想放什麼都可以,主要是對結構體的定義聲明,我這裏想提一下,

爲什麼第一個結構體和第二個結構 struct 後面一個有帶名字一個沒帶呢?

struct xxx 這個xxx實際上是這個結構的標識符,xxx 單獨使用是沒有意義的,但是 struct xxx 是有意義的,它是一種自定義的結構類型 (跟 int float 這種預定義類型是同一階級的),因此我們如果想聲明某個變量的內部結構是我們定義的這種,只需要這樣聲明:struct xxx A,但是這樣聲明顯很累贅,因此一般來說我們會使用 typedef 來聲明別名,例如第一個 QNode === struct QNode, 此時,struct QNode A 就可以簡寫成  QNode A

② 鏈隊的初始化

Status InitQueue(LinkQueue &Q)
{
    Q.front = Q.rear = new QNode;//生成新的節點,並且將頭尾指針均指向此節點
    Q.front->next = NULL; //將頭指針的下個指向定爲NULL
    return OK;
}

③ 鏈隊的入隊

Status EnQueue(LinkQueue &Q, QElemType e)
{
    p=new QNode; //創建一個新的結點
    p->data = e; //結點的數據域賦值爲 e
    p->next = NULL; //p沒有下一個人跟隨,置爲 NULL
    Q.rear->next=p; //把尾指針指向 p,說明 p 目前是鏈表的最後一個元素
    return 0K;
}

④ 鏈隊的出隊

Status DeQueue(LinkQueue &Q, QElemType &e)
{
    if (Q.front == Q.rear) return ERROR; //頭尾相接說明爲空隊列

    p=Q.front->next; //保留隊頭元素
    e = p->data; //保留隊頭元素的數據域
    
    Q.front->next = p->next; ///修改頭指針,讓他指向它原來的下一位
    if (Q.rear == p) Q.rear=Q.front; //如果是最後一個元素出隊,那麼尾指針就不知道指誰了,此時應該和頭指針指向一處

    delete p; //釋放 p 的內存

    return OK;
}

⑤ 鏈隊的隊頭元素

SElemType GetHead(LinkQueue Q)
{
    if( Q.front != Q.rear) { //隊列非空
        return Q.front->next->data;  //返回隊頭元素的數據域
    }
}

過了一段時間重看這段僞代碼,是有幾個地方比較模糊的,比如說爲什麼需要尾指針呢?

我嘗試思考,如果只有頭指針,沒有尾指針會出現什麼後果,例如在入隊的時候,我們確實可以根據頭指針找到隊頭元素,而且Q.front->next == 隊頭 ,Q.front->next->next == 隊頭第二個人,我們確實可以根據這樣的遞歸來找到需要的元素,但是入隊怎麼辦呢,難道我每次入隊都要從頭遍歷一下來找到最後一個元素,然後纔在最後一個元素後面進行入隊操作,顯然就是浪費時間。所以多加個尾指針是有意義的。

結點是一個開闢的空間,裏面有存儲所需要存儲的東西,我們每次入隊一個元素,都會增加一個口袋,裏面放着需要的東西,出隊則直接操作頭指針,入隊則直接操作尾指針,我們通過這個 ->next 的方式指向,將這個原本毫無關係的口袋拿一條有方向的繩子綁了起來,形成一個具有多個口袋的繩子。

如果我們用這種實際的例子去讓這種思想不再抽象化,就會較容易的理解程序爲什麼要這麼設計,現實例子類比的方法適用於很多生活中的算法,畢竟程序是因生活而生。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章