队列
1.定义
队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(head)进行删除操作,而在表的后端(tail)进行插入操作,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。
队列的数据元素又称为队列元素。在队列中插入一个队列元素称为入队,从队列中删除一个队列元素称为出队。因为队列只允许在一端插入,在另一端删除,所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为先进先出(FIFO—first in first out)线性表。
2.线性队列
数据结构 = 结构定义 + 结构操作。
首先,对于线性队列的结构定义如下
1 typedef struct Queue { 2 int *data; 3 int head, tail, length; 4 } Queue;
data是一段用来存储数据元素的内存地址,由于我们的存储的数据类型为int,所以date的数据类型是int*,head来表示队头,tail来表示队尾,length来表示队列的容量大小。
需要注意的是,线性队列有两种实现方式:1,tail指针指向最后一个元素位置的地址;2.tail指针指向最后一个元素的下一个位置的地址。在这里我们采用第二种方式来实现线性队列。
然后,对于线性队列的结构操作如下:
1 Queue *init(int n); 2 void empty(Queue *q); 3 int push(Queue *q, int val); 4 int pop(Queue *q); 5 void output(Queue *q); 6 void clear(Queue *q); 7 int front(Queue *q);
2.1 队列初始化
1 /* 2 描述: 3 初始化一个容量大小为n的先行队列。 4 参数: 5 n:表示初始化时,队列的容量大小 6 返回值: 7 成功返回队列q的地址 8 */ 9 Queue *init(int n) { 10 Queue *q = (Queue *)malloc(sizeof(Queue)); 11 q->data = (int *)malloc(sizeof(int) * n); 12 q->length = n; 13 q->head = q->tail = 0; 14 return q; 15 }
由于我们存储的元素类型是整型数据,所以date是int*类型,另外我们初始化的时候指定了队列的容量大小,并且初始化后的队列没有任何元素,tail和head都指向0.
2.2 销毁队列
1 /* 2 描述: 3 销毁一个线性队列q 4 参数: 5 q: 线性队列的地址 6 返回值: 7 无返回值。 8 */ 9 void clear(Queue *q) { 10 if (q == NULL) return ; 11 free(q->data); 12 free(q); 13 return ; 14 }
销毁一个队列的操作会用到free函数,所以我们必须事先判断队列指针是否为空,另外我们必须先释放存放队列元素的那一段内存空间,再释放队列。
2.3 返回队头元素
1 /* 2 描述: 3 返回队头元素。 4 参数: 5 线性队列的地址 6 返回值: 7 队头元素的值 8 */ 9 int front(Queue *q) { 10 return q->data[q->head]; 11 }
返回队头元素就是head指针指向的元素。
2.4 判断队列是否为空
1 /* 2 描述: 3 判断队列是否为空 4 参数: 5 线性队列的地址 6 返回值: 7 队列为空返回1,否则返回0 8 */ 9 int empty(Queue *q) { 10 return q->head == q->tail; 11 }
判断队列是否为空,我们只需要关注head的指向和tail的指向是否一样,即head是否等于tail。
2.5 入队操作
1 /* 2 描述: 3 入队一个元素 4 参数: 5 q: 线性队列的地址 6 val:要入队的元素 7 返回值: 8 入队成功返回1,否则返回0 9 */ 10 int push(Queue *q, int val) { 11 if (q == NULL) return 0; 12 if (q->tail == q->length) return 0; 13 q->data[q->tail] = val; 14 q->tail++; 15 return 1; 16 }
首先我们必须进行队列是否存在的判断,即11行。由于我们是用tail指针指向最后一个元素的下一个位置的地址的方式来实现线性队列的,所以我们必须在12行来进行判断队列是否已经满了,故当tail的值与length相等的时候,表示队列已溢出(这里的溢出可能是假溢出,后续会解释)。13~14行就是存储入队的元素,因为tail是表示最后一个元素的下一个位置,所以我们必须先存元素后再对tail后移。
2.6 出队操作
1 /* 2 描述: 3 出队一个元素。 4 参数: 5 q: 线性队列的地址 6 返回值: 7 出队成功返回1,否则返回0 8 */ 9 int pop(Queue *q) { 10 if (q == NULL) return 0; 11 if (empty(q)) return 0; 12 q->head++; 13 return 1; 14 }
出队操作很简单,先进行对传入的队列地址q的合理性判断,再判断队列是否为空,然后只需要进行头指针后移就可以表示出队操作了。
2.7 输出操作
1 /* 2 描述: 3 以指定格式输出一个队列的所有元素 4 参数: 5 q:线性队列的地址 6 返回值: 7 无返回值。 8 */ 9 void output(Queue *q) { 10 if (q == NULL) return ; 11 printf("Queue: ["); 12 for (int i = q->head, j = 0; i < q->tail; i++, j++) { 13 j && printf(", "); 14 printf("%d", q->data[i]); 15 } 16 printf("]\n"); 17 }
以指定格式[e1, e2, ..., en]输出一个队列的所有元素。
2.8 测试程序
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <time.h> 4 5 #define COLOR(a, b) "\033[" #b "m" a "\033[0m" 6 #define RED(a) COLOR(a, 31) 7 typedef struct Queue { 8 int *data; 9 int head, tail, length; 10 } Queue; 11 12 Queue *init(int n); 13 int empty(Queue *q); 14 int push(Queue *q, int val); 15 int pop(Queue *q); 16 void output(Queue *q); 17 void clear(Queue *q); 18 int front(Queue *q); 19 20 int main(void) { 21 srand(time(0)); 22 #define max_op 20 23 Queue *q = init(10); 24 for (int i = 0; i < max_op; i++) { 25 int val = rand() % 100; 26 int op = rand() % 4; 27 printf(RED("operation: %d\n"), i); 28 switch (op) { 29 case 0: 30 case 1: 31 case 2: { 32 printf("Push %d to the Queue!", val); 33 printf("result = %d\n", push(q, val)); 34 } break; 35 case 3: { 36 printf("Pop %d from the Queue! ", front(q)); 37 printf("result = %d\n", pop(q)); 38 } break; 39 } 40 output(q), printf("\n"); 41 } 42 #undef max_op 43 clear(q); 44 return 0; 45 } 46 //队列有两种实现方式 47 //1.tail指针指向最后一个元素位置的地址 48 //2.tail指针指向最后一个元素的下一个位置的地址 49 //我们采用第二种 50 Queue *init(int n) { 51 Queue *q = (Queue *)malloc(sizeof(Queue)); 52 q->data = (int *)malloc(sizeof(int) * n); 53 q->length = n; 54 q->head = q->tail = 0; 55 return q; 56 } 57 58 int front(Queue *q) { 59 return q->data[q->head]; 60 } 61 62 int empty(Queue *q) { 63 return q->head == q->tail; 64 } 65 66 int push(Queue *q, int val) { 67 if (q == NULL) return 0; 68 if (q->tail == q->length) return 0; 69 q->data[q->tail] = val; 70 q->tail++; 71 return 1; 72 } 73 74 int pop(Queue *q) { 75 if (q == NULL) return 0; 76 if (empty(q)) return 0; 77 q->head++; 78 return 1; 79 } 80 81 void output(Queue *q) { 82 if (q == NULL) return ; 83 printf("Queue: ["); 84 for (int i = q->head, j = 0; i < q->tail; i++, j++) { 85 j && printf(", "); 86 printf("%d", q->data[i]); 87 } 88 printf("]\n"); 89 } 90 91 void clear(Queue *q) { 92 if (q == NULL) return ; 93 free(q->data); 94 free(q); 95 return ; 96 }
测试程序中,我们初始化了一个容量大小为10的队列,并对其进行20次入队或者出队操作,其中大概3/4的操作是入队,1/4的操作是出队,每次操作完毕我们都输出整个队列的所有元素。
编译并测试。
operation: 0 Push 4 to the Queue!result = 1 Queue: [4] operation: 1 Pop 4 from the Queue! result = 1 Queue: [] operation: 2 Push 50 to the Queue!result = 1 Queue: [50] operation: 3 Push 10 to the Queue!result = 1 Queue: [50, 10] operation: 4 Push 62 to the Queue!result = 1 Queue: [50, 10, 62] operation: 5 Push 36 to the Queue!result = 1 Queue: [50, 10, 62, 36] operation: 6 Push 10 to the Queue!result = 1 Queue: [50, 10, 62, 36, 10] operation: 7 Push 49 to the Queue!result = 1 Queue: [50, 10, 62, 36, 10, 49] operation: 8 Push 83 to the Queue!result = 1 Queue: [50, 10, 62, 36, 10, 49, 83] operation: 9 Push 59 to the Queue!result = 1 Queue: [50, 10, 62, 36, 10, 49, 83, 59] operation: 10 Pop 50 from the Queue! result = 1 Queue: [10, 62, 36, 10, 49, 83, 59] operation: 11 Push 66 to the Queue!result = 1 Queue: [10, 62, 36, 10, 49, 83, 59, 66] operation: 12 Push 79 to the Queue!result = 0 Queue: [10, 62, 36, 10, 49, 83, 59, 66] operation: 13 Push 66 to the Queue!result = 0 Queue: [10, 62, 36, 10, 49, 83, 59, 66] operation: 14 Pop 10 from the Queue! result = 1 Queue: [62, 36, 10, 49, 83, 59, 66] operation: 15 Pop 62 from the Queue! result = 1 Queue: [36, 10, 49, 83, 59, 66] operation: 16 Push 16 to the Queue!result = 0 Queue: [36, 10, 49, 83, 59, 66] operation: 17 Pop 36 from the Queue! result = 1 Queue: [10, 49, 83, 59, 66] operation: 18 Push 11 to the Queue!result = 0 Queue: [10, 49, 83, 59, 66] operation: 19 Push 88 to the Queue!result = 0 Queue: [10, 49, 83, 59, 66]
从测试结果,我们可以看到在第12次是入队操作,并且该操作执行失败,但是整个队列的元素只有8个,而我们的队列的容量是10,所以说明这时候已经出现了假溢出。为了解决假溢出的现象,我们需要另一种队列实现--循环队列。
3.循环队列
循环队列的结构定义如下:
typedef struct Queue { int *data; int head, tail, length, cnt; } Queue;
循环队列的结构定义只比线性队列的结构定义多了一个cnt来表示队列的元素个数。
循环队列的结构操作如下:
1 Queue *init(int n); 2 void empty(Queue *q); 3 int push(Queue *q, int val); 4 int pop(Queue *q); 5 void output(Queue *q); 6 void clear(Queue *q); 7 int front(Queue *q);
循环队列的结构操作和线性队列的结构操作都一样,但是部分操作的内部逻辑我们必须进行修改,这部分需要修改的操作为init,empty, push,pop, output。接下来我们分别给出这部分操作的代码并分析。
3.1 初始化
1 Queue *init(int n) { 2 Queue *q = (Queue *)malloc(sizeof(Queue)); 3 q->data = (int *)malloc(sizeof(int) * n); 4 q->length = n; 5 q->head = q->tail = 0; 6 q->cnt = 0; 7 return q; 8 }
由于多了一个cnt成员来表示队列的元素个数,所以初始化的时候cnt为0.
3.2 判断队列是否为空
1 int empty(Queue *q) { 2 return q->cnt == 0; 3 }
有了cnt来表示队列元素个数,我们就可以直接判断cnt是否等于0来判断队列是否为空。
3.3 入队操作
1 int push(Queue *q, int val) { 2 if (q == NULL) return 0; 3 if (q->cnt == q->length) { 4 if (!expand(q)) return 0; 5 printf(GREEN("expand queue successful!, queue->length:%d\n"), q->length); 6 } 7 q->data[q->tail++] = val; 8 if (q->tail == q->length) q->tail = 0; // q->tail %= q->length 9 q->cnt += 1; 10 return 1; 11 }
首先第2行还是常规的q指针合理性判断,3~6行是判断当队列满了之后,进行扩容操作,如果扩容失败则返回0表示插入失败,扩容成功则打印成功语句并继续入队元素,至于expand扩容操作,我们随后补充。到了第7行代码,这时候是说明队列还没有满,我们入队并后移tail指针,这时候我们必须对tail进行判断是否等于length,如果等于length,则必须把tail重新指向0,然后再让cnt加1表示元素数量增加一个。这里需要注意的是,因为我们有了cnt跟length对比来判断队列是否还有空间可供新元素入队,所以只要逻辑到了第7行,我们的入队操作是绝对安全的。
3.4 出队操作
1 int pop(Queue *q) { 2 if (q == NULL) return 0; 3 if (empty(q)) return 0; 4 q->head++; 5 if (q->head == q->length) q->head = 0; //q->head %= q->length; 6 q->cnt -= 1; 7 return 1; 8 }
同样出队操作除了第2行常规合理性判断和第3行队列判空操作,我们要关心的只是head指针是否等于length,如果等于length则重新指向0.
3.5 输出队列所有元素操作
1 void output(Queue *q) { 2 if (q == NULL) return ; 3 printf("Queue: ["); 4 for (int i = 0; i < q->cnt; i++) { 5 i && printf(", "); 6 printf("%d", q->data[(i + q->head) % q->length]); 7 } 8 printf("]\n"); 9 }
输出函数只需要从head开始一直往后输出每个元素,当后移到length的时候,要重新从0开始后移输出,这时候我们只需要多一个下标跟length取余操作即可实现,可以理解一下第6行代码。
3.6 扩容操作
循环队列我们可以增添一个扩容操作,当队列真的溢出的时候,我们需要支持扩容,扩容代码如下:
1 /* 2 描述: 3 扩容队列q的容量大小 4 参数: 5 q:需要扩容的队列的地址 6 返回值: 7 成功返回1,失败返回0 8 */ 9 int expand(Queue *q) { 10 int extra_size = q->length; 11 int *p; 12 while (extra_size) { 13 p = (int *)malloc(sizeof(int) * (q->length + extra_size)); 14 if (p) break; 15 extra_size >>= 1; 16 } 17 if (p == NULL) return 0; 18 for (int i = q->head, j = 0; j < q->cnt; j++) { 19 p[j] = q->data[(i + j) % q->length]; 20 } 21 free(q->data); 22 q->data = p; 23 q->length += extra_size; 24 q->head = 0; 25 q->tail = q->cnt; 26 return 1; 27 }
首先第10行,我们默认扩容多原来的容量大小的1倍,在12到16行,我们循环去扩容,因为我们扩容的容量可能太大,系统的内存存在不足的可能,所以当申请扩容的容量extra_size太大的时候,我们缩小extra_size,为原来的1/2(即第15行代码),如果中途某个容量的内存申请成功了,我们就推出直接推出(即第14行代码)。到了第18行,则说明我们申请到了一段新的内存,我们这时候需要把原来队列的所有元素拷贝到新的扩容内存中去,即第18~20行代码;到了21行,我们就可以把原来的元素的内存释放掉,再更新队列的信息。至此我们循环队列的所有操作都实现完毕,我们可以编写测试代码。
4.测试程序
1 //循环队列 2 #include <stdio.h> 3 #include <stdlib.h> 4 #include <time.h> 5 6 #define COLOR(a, b) "\033[" #b "m" a "\033[0m" 7 #define RED(a) COLOR(a, 31) 8 #define GREEN(a) COLOR(a, 32) 9 10 typedef struct Queue { 11 int *data; 12 int head, tail, length, cnt; 13 } Queue; 14 15 //队列有两种实现方式 16 //1.tail指针指向最后一个元素位置的地址 17 //2.tail指针指向最后一个元素的下一个位置的地址 18 //我们采用第二种 19 Queue *init(int n) { 20 Queue *q = (Queue *)malloc(sizeof(Queue)); 21 q->data = (int *)malloc(sizeof(int) * n); 22 q->length = n; 23 q->head = q->tail = 0; 24 q->cnt = 0; 25 return q; 26 } 27 28 int front(Queue *q) { 29 return q->data[q->head]; 30 } 31 32 int empty(Queue *q) { 33 return q->cnt == 0; 34 } 35 36 int expand(Queue *q) { 37 int extra_size = q->length; 38 int *p; 39 while (extra_size) { 40 p = (int *)malloc(sizeof(int) * (q->length + extra_size)); 41 if (p) break; 42 extra_size >>= 1; 43 } 44 if (p == NULL) return 0; 45 for (int i = q->head, j = 0; j < q->cnt; j++) { 46 p[j] = q->data[(i + j) % q->length]; 47 } 48 free(q->data); 49 q->data = p; 50 q->length += extra_size; 51 q->head = 0; 52 q->tail = q->cnt; 53 return 1; 54 } 55 56 int push(Queue *q, int val) { 57 if (q == NULL) return 0; 58 if (q->cnt == q->length) { 59 if (!expand(q)) return 0; 60 printf(GREEN("expand queue successful!, queue->length:%d\n"), q->length); 61 } 62 q->data[q->tail++] = val; 63 if (q->tail == q->length) q->tail = 0; // q->tail %= q->length 64 q->cnt += 1; 65 return 1; 66 } 67 68 int pop(Queue *q) { 69 if (q == NULL) return 0; 70 if (empty(q)) return 0; 71 q->head++; 72 if (q->head == q->length) q->head = 0; //q->head %= q->length; 73 q->cnt -= 1; 74 return 1; 75 } 76 77 void output(Queue *q) { 78 if (q == NULL) return ; 79 printf("Queue: ["); 80 for (int i = 0; i < q->cnt; i++) { 81 i && printf(", "); 82 printf("%d", q->data[(i + q->head) % q->length]); 83 } 84 printf("]\n"); 85 } 86 87 void clear(Queue *q) { 88 if (q == NULL) return ; 89 free(q->data); 90 free(q); 91 return ; 92 } 93 94 int main(void) { 95 srand(time(0)); 96 #define max_op 20 97 Queue *q = init(2); 98 for (int i = 0; i < max_op; i++) { 99 int val = rand() % 100; 100 int op = rand() % 4; 101 printf(RED("operation: %d\n"), i); 102 switch (op) { 103 case 0: 104 case 1: 105 case 2: { 106 printf("Push %d to the Queue!", val); 107 printf("result = %d\n", push(q, val)); 108 } break; 109 case 3: { 110 printf("Pop %d from the Queue! ", front(q)); 111 printf("result = %d\n", pop(q)); 112 } break; 113 } 114 output(q), printf("\n"); 115 } 116 printf("tail: %d\n", q->tail); 117 #undef max_op 118 clear(q); 119 return 0; 120 }
编译并测试:
operation: 0 Push 62 to the Queue!result = 1 Queue: [62] operation: 1 Push 57 to the Queue!result = 1 Queue: [62, 57] operation: 2 Push 76 to the Queue!expand queue successful!, queue->length:4 result = 1 Queue: [62, 57, 76] operation: 3 Pop 62 from the Queue! result = 1 Queue: [57, 76] operation: 4 Push 26 to the Queue!result = 1 Queue: [57, 76, 26] operation: 5 Push 19 to the Queue!result = 1 Queue: [57, 76, 26, 19] operation: 6 Push 95 to the Queue!expand queue successful!, queue->length:8 result = 1 Queue: [57, 76, 26, 19, 95] operation: 7 Push 29 to the Queue!result = 1 Queue: [57, 76, 26, 19, 95, 29] operation: 8 Push 35 to the Queue!result = 1 Queue: [57, 76, 26, 19, 95, 29, 35] operation: 9 Pop 57 from the Queue! result = 1 Queue: [76, 26, 19, 95, 29, 35] operation: 10 Pop 76 from the Queue! result = 1 Queue: [26, 19, 95, 29, 35] operation: 11 Push 99 to the Queue!result = 1 Queue: [26, 19, 95, 29, 35, 99] operation: 12 Push 82 to the Queue!result = 1 Queue: [26, 19, 95, 29, 35, 99, 82] operation: 13 Pop 26 from the Queue! result = 1 Queue: [19, 95, 29, 35, 99, 82] operation: 14 Push 14 to the Queue!result = 1 Queue: [19, 95, 29, 35, 99, 82, 14] operation: 15 Push 65 to the Queue!result = 1 Queue: [19, 95, 29, 35, 99, 82, 14, 65] operation: 16 Push 1 to the Queue!expand queue successful!, queue->length:16 result = 1 Queue: [19, 95, 29, 35, 99, 82, 14, 65, 1] operation: 17 Push 98 to the Queue!result = 1 Queue: [19, 95, 29, 35, 99, 82, 14, 65, 1, 98] operation: 18 Pop 19 from the Queue! result = 1 Queue: [95, 29, 35, 99, 82, 14, 65, 1, 98] operation: 19 Push 73 to the Queue!result = 1 Queue: [95, 29, 35, 99, 82, 14, 65, 1, 98, 73] tail: 11