【C語言->數據結構與算法】->隊列->循環數組的應用&隊列工具庫的創建

Ⅰ 隊列的定義

隊列是一種特殊的線性表,特殊之處在於它只允許在表的前端(front)進行刪除操作,而在表的後端(rear)進行插入操作,和棧一樣,隊列是一種操作受限制的線性表。進行插入操作的端稱爲隊尾,進行刪除操作的端稱爲隊頭。

隊列的特點:先進先出
堆棧的特點:先進後出

在這裏插入圖片描述

Ⅱ 隊列的實現

隊列有兩種實現的方法,鏈表和數組。對於隊列來說,用鏈表是很麻煩的,爲了提高效率和簡化操作,隊列還是用數組來實現比較好。

A. 普通數組的不合理之處

我先建立一個數組。
在這裏插入圖片描述
在還沒有數據存入的時候,隊首隊尾指針都在下標爲0的位置,當入隊列時,數據存入隊首指針現在的位置,並且隊首指針後移。
在這裏插入圖片描述
這時候再出隊列,隊尾指針指向的位置上的元素出去。
在這個過程中,隊首和隊尾的指針都是不斷增大的,但是數組的個數是有限的。
在這裏插入圖片描述
導致即使前面的數據都已出隊列,數組還有這麼多空間,但是無法使用,因爲隊尾隊首指針在不斷向前。

B. 循環數組的應用

在普通的數組不能很好地完成隊列操作時,我在此引入循環數組
循環數組我們可以用下圖來理解,類似於一個鐘錶:
在這裏插入圖片描述
當入隊列時,
在這裏插入圖片描述
這就實現了從隊首出隊列,從隊尾入隊列。
在這裏插入圖片描述
還是剛纔普通數組難以解決的問題,當頭指針在12,尾指針在13,這時候入隊列需要將其放在尾指針指向的位置,並將尾指針+1.
但是此時13+1需要將尾指針變成0,要怎麼做?

這就是循環數組的精妙之處了。以這個例子來看,這是個容量爲14的數組。我們設下標爲index,容量爲capacity。

(index + 1) % capacity就可以做到將下標增加,並使其循環。

以剛纔爲例,現在尾指針爲13,要入隊列,先將數據放在尾指針的位置,即13,再將尾指針(13 + 1) % 14 = 0,這就實現了循環。

那麼數組裏的有效數據個數怎麼計算呢?我們來看下面兩個例子。
在這裏插入圖片描述
在這裏插入圖片描述
這兩種情況,剛好是head和tail的值顛倒過來,如果我們要計算有效數據個數,就需要一個公式:(tail - head + capacity) % capacity

但是這時候我們會發現,當隊列滿和隊列空的時候,計算出來的數據個數都是0。
所以要判斷隊列空還是滿,我們還需要再加入一個判斷,即上一次操作是入隊列還是出隊列。

如果上一次操作是入隊列,那麼如果計算出來數據個數爲0,就可以判斷這個隊列是滿的,反之上一次操作是出隊列,那麼隊列就是空的。

Ⅲ 隊列工具庫的創建

循環數組的規律清楚了以後,我們就可以動手來實現隊列的工具庫了。

A. 隊列的表示

首先我們先來表示隊列。在之前的分析中,可以知道,要建立一個隊列,需要隊首指針head,需要隊尾指針tail,要建立數組我們還需要知道數組的大小capacity,然後我們還需要知道上一次操作是什麼,所以衆多的元素我們需要建立一個結構體。

typedef struct QUEUE {
	int head;
	int tail;
	int capacity;
	void **data;
	boolean lastAction;
}QUEUE;

這樣我們就建立了一個隊列的控制頭。大家可能會對這個void **data有點疑惑。爲什麼要這麼寫呢?
這樣其實是建立了一個數組,其存放的元素是指針。因爲我們不知道用戶到底要在隊列裏錄入什麼類型的數據,所以我們無論其錄入什麼數據,我們都將其地址存入數組中,這樣就可以實現通用數組。因爲無論你的數據是什麼類型,幾個字節,其地址都是四個字節,所以可以通過存放地址,來保證數組的元素類型統一。這其實就是一個指針數組。

關於指針數組有疑惑的同學也可以去看我下面這篇文章👇
【C語言基礎】->指針與二維數組->同一個人的換裝遊戲

另外還有一個說明,boolean是我自己在我的頭文件中定義的一個類型,其本質是unsigned char

#ifndef _TYZ_QUEUE_H_
#define _TYZ_QUEUE_H_

#include "tyz.h"

#define  IN   2
#define  OUT  3

typedef struct QUEUE {
	int head;
	int tail;
	int capacity;
	void **data;
	boolean lastAction;
}QUEUE;

#endif

通過對INOUT的宏定義,我們來判斷上一次操作到底是什麼。

B. 初始化&銷燬

a. 初始化

要使用隊列,最開始當然要先初始化。代碼如下👇

boolean initQueue(QUEUE **queue, int capacity) {
	if (NULL == queue || NULL != *queue || capacity <= 0) {
		return FALSE;
	}
	*queue = (QUEUE *) calloc(sizeof(QUEUE), 1);

	if (NULL == queue) {
		return FALSE;
	}
	(*queue)->data = (void **) calloc(sizeof(void *), capacity);

	if (NULL == (*queue)->data) {
		free(*queue);
		*queue = NULL;
		return FALSE;
	}
	(*queue)->head = 0;
	(*queue)->tail = 0;
	(*queue)->capacity = capacity;
	(*queue)->lastAction = OUT;

	return TRUE;
}

可能會有人對我的形參有點疑惑,爲什麼是指針的指針。QUEUE **queue。我在下面列出調用這個函數的代碼做爲比較。

	QUEUE *queue = NULL;;

	initQueue(&queue, 30);

可以看到我們先定義了一個指針,指向控制頭QUEUE,根據一個著名的例子,用指針交換兩個變量的值,如果你要改變實參的值,傳遞形參就應該把它的地址傳過去,否則是無法改變它的值的。所以參考這裏,我們需要初始化這個QUEUE *queue,就需要傳遞它的地址,即&queue。其對應的形參就是QUEUE **queue了。

b. 銷燬

申請了空間首先就要考慮釋放空間,所以接下來要寫的就是銷燬隊列。👇

void destoryQueue(QUEUE **queue) {
	if (NULL == queue || NULL != *queue) {
		return;
	}
	free((*queue)->data);
	free(*queue);
	*queue = NULL;
}

這裏的原理是一樣的,要傳遞(QUEUE **queue)。因爲要銷燬指向控制頭的那個指針。

C. 判斷隊列空&隊列滿

在之前的分析中,我們已經知道了如何判斷空滿,只需要滿足兩個條件,一個是headtail值相等,一個是上一次操作是入隊列還是出隊列。

a. 判斷隊列滿

boolean isQueneFull(const QUEUE *queue) {
	if (NULL == queue) {
		return TRUE;
	}
	return IN == queue->lastAction && queue->head == queue->tail;
}

如果傳遞進去的是一個無效的指針,那麼我將其看做的滿的。因爲只有要入隊列的時候,需要判斷隊列是不是滿的,這時候無效的指針是無法將數據入隊列的,所以我將其看做是滿的,意思也是不能再入隊列了。

b. 判斷隊列空

boolean isQueneEmpty(const QUEUE *queue) {
	if (NULL == queue) {
		return TRUE;
	}
	return OUT == queue->lastAction && queue->head == queue->tail;
}

遵從和上面一樣的原則。

D. 入隊列&出隊列

a. 入隊列

boolean in(QUEUE *queue, void *data) {
	if (NULL == queue || isQueneFull(queue)) {
		return FALSE;
	}
	queue->data[queue->tail] = data;
	queue->tail = (queue->tail + 1) % queue->capacity;
	queue->lastAction = IN;

	return TRUE;
}

這個邏輯就是之前分析的,從尾指針的地方入隊列,並將尾指針向前走一格,方式就是(queue->tail + 1) % queue->capacity

b. 出隊列

boolean out(QUEUE *queue, void *data) {
	if (NULL == queue || isQueneEmpty(queue)) {
		return FALSE;
	}
	data = queue->data[queue->head];
	queue->head = (queue->head + 1) % queue->capacity;
	queue->lastAction = OUT;
}

其邏輯是一樣的,從隊首出去,然後將頭指針向前挪動一格。

出隊列和入隊列都是很簡單的,需要注意的就是要將lastAction設置爲對應的INOUT

E. 讀取隊首數據

讀取隊首數據是和出隊列差不多的,只是不需要將頭指針挪動。

void readHead(const QUEUE *queue, void *data) {
	if (NULL == queue || isQueneEmpty(queue)) {
		return;
	}
	data = queue->data[queue->head];
}

這樣我們就完成了一個隊列的工具庫,其基本功能就是入隊列和出隊列。

Ⅳ. 完整代碼

首先是我的自己的頭文件tyz.h👇

#ifndef _TYZ_H_
#define _TYZ_H_

#define TRUE 1
#define FALSE 0
#define NOT_FOUND -1

#define SET(value, index)    (value |= 1 << (index ^ 7))
#define CLEAR(value, index)  (value &= ~(1 << (index ^ 7)))
#define GET(value, index)    ((value) & (1 << (index ^ 7)) != 0)

typedef unsigned char boolean;
typedef unsigned int u32;
typedef unsigned short u16;
typedef boolean u8;

int skipBlank(const char *str);
boolean isRealStart(int ch);

#endif

然後是隊列的頭文件,queue.h👇

#ifndef _TYZ_QUEUE_H_
#define _TYZ_QUEUE_H_

#include "tyz.h"

#define  IN   2
#define  OUT  3

typedef struct QUEUE {
	int head;
	int tail;
	int capacity;
	void **data;
	boolean lastAction;
}QUEUE;

boolean initQueue(QUEUE **queue, int capacity);
void destoryQueue(QUEUE **queue);
boolean isQueneFull(const QUEUE *queue);
boolean isQueneEmpty(const QUEUE *queue);
boolean in(QUEUE *queue, void *data);
boolean out(QUEUE *queue, void *data);
void readHead(const QUEUE *queue, void *data);

#endif

最後是隊列的函數部分代碼👇

#include <stdio.h>
#include <malloc.h>

#include "tyz.h"
#include "queue.h"

void readHead(const QUEUE *queue, void *data) {
	if (NULL == queue || isQueneEmpty(queue)) {
		return;
	}
	data = queue->data[queue->head];
}

boolean out(QUEUE *queue, void *data) {
	if (NULL == queue || isQueneEmpty(queue)) {
		return FALSE;
	}
	data = queue->data[queue->head];
	queue->head = (queue->head + 1) % queue->capacity;
	queue->lastAction = OUT;
}

boolean in(QUEUE *queue, void *data) {
	if (NULL == queue || isQueneFull(queue)) {
		return FALSE;
	}
	queue->data[queue->tail] = data;
	queue->tail = (queue->tail + 1) % queue->capacity;
	queue->lastAction = IN;

	return TRUE;
}

boolean isQueneEmpty(const QUEUE *queue) {
	if (NULL == queue) {
		return TRUE;
	}
	return OUT == queue->lastAction && queue->head == queue->tail;
}

boolean isQueneFull(const QUEUE *queue) {
	if (NULL == queue) {
		return TRUE;
	}
	return IN == queue->lastAction && queue->head == queue->tail;
}

void destoryQueue(QUEUE **queue) {
	if (NULL == queue || NULL != *queue) {
		return;
	}
	free((*queue)->data);
	free(*queue);
	*queue = NULL;
}

boolean initQueue(QUEUE **queue, int capacity) {
	if (NULL == queue || NULL != *queue || capacity <= 0) {
		return FALSE;
	}
	*queue = (QUEUE *) calloc(sizeof(QUEUE), 1);

	if (NULL == queue) {
		return FALSE;
	}
	(*queue)->data = (void **) calloc(sizeof(void *), capacity);

	if (NULL == (*queue)->data) {
		free(*queue);
		*queue = NULL;
		return FALSE;
	}
	(*queue)->head = 0;
	(*queue)->tail = 0;
	(*queue)->capacity = capacity;
	(*queue)->lastAction = OUT;

	return TRUE;
}

這樣我們就完成了隊列工具庫的創建。
如果對指針的操作還有疑問的可以看我下面的文章👇
【C語言基礎】->指針與二維數組->同一個人的換裝遊戲

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