目录
队列定义
队列是一种有序的数据结构,它遵循先进先出(First In First Out 简称 FIFO)的原则。队列的操作可以在队尾(称为入队)添加元素,并且在队头(称为出队)移除元素。新元素总是被添加到队列的尾部,而最早添加的元素总是在队列的头部。
可以将队列比作人们排队等候办理业务的场景,新来的人会加入队列的尾部,而需要办理业务的人会从队列的头部离开。这种先到先办理的原则确保了队列操作的顺序性。
队列实现
之前我们讲到栈的时候推荐用顺序表,在这里,我们队列实现用的是链表,对于队列而言,用链表会比用顺序表会更加方便快捷。
时间复杂度简单分析
顺序表: 入队O(1) 出队O(n)
链表 入队O(1) 出队O(1)
用顺序表出队的时候还要考虑将队列中所有节点都向前移一位
链表则直接把头指针换成头结点的后一个,并头结点释放即可
队列声明
队列主要的点:
1.需要头指针和尾指针,头指针的类型为节点类型(指针要指向节点,才能得到相应的数据)
2.节点类型需要存储两个变量,一个是数据变量,一个是指向下一个节点的指针变量(链表特点)
typedef int DataType;//重定义类型,增强维护性
typedef struct QueueNode
{
DataType x;
struct QueueNode* next;//指向下一节点
}QNode;
typedef struct Queue
{
QNode* head;//队头指针
QNode* tail;//队尾指针
}Queue;
将int类型重定义为DataType,方便维护,以后如果队列存储char类型的数据,直接将typedef中的int改为char即可
小插曲:为什么节点类型存储的是struct QueueNode*而不是QNode*?其实这两个都是同一个类型,参考int和DataType,用struct QueueNode*因为QNode在此时结构体里面还没有定义,它是在结构体的末尾处才声明重定义这个类型的。在下面队列结构体中可以用QNode*,因为此时QNode*在上一个结构体的尾端定义已经定义好了。
接口函数实现
初始化队列
队列无任何节点,初始化的时候将头指针和尾指针同时赋为NULL
//初始化
void InitQueue(Queue* pq)
{
//队列存在
assert(pq);
pq->head = pq->tail = NULL;//头指针尾指针同时为NULL
}
销毁队列
队列销毁节点要一个一个释放
销毁想一件事:从头销毁还是从尾销毁?仔细想想会发现从头销毁的话其实会比从尾开始快捷简易,从尾销毁每次需要找到尾巴的前一个指针,链表找下一个节点容易,找上一个节点就有点胸口碎大石了。
//队列销毁
void DestroyQueue(Queue* pq)
{
assert(pq);
QNode* cur = pq->head;
//cur指向head一直到指向tail,当tail->next赋给cur,cur循环结束
while (cur)
{
QNode* next = cur->next;
free(cur);
cur = next;
}
}
可以想循环最后一次指针cur怎么样了,被赋值为什么。
入队
入队的主要操作:
1.将传进来的数据打包成节点(新节点)
2.放在队列的尾端
第二点的时候,我们上来就可以写个让尾指针的下一个节点指向新节点并让尾指针指向新节点的代码,但是仔细一想,代码会有缺漏,为什么?万一尾指针为NULL怎么办(初始化的时候头指针和尾指针同时为NULL),这个时候你对尾指针next,程序可就会出错了,所以这个时候还需要一个判断
综合一下:
情况一,当队列没有节点
情况二,当队列所含节点大于等于1
//队尾入
void PushQueue(Queue* pq,DataType x)
{
assert(pq);
QNode* node = CreatQNode(x);
//队列无节点
if (pq->head == NULL)
{
pq->head = pq->tail = node;
}
else
{
//队列有一个以上节点
//node接入尾节点
pq->tail->next = node;
//尾节点指针指向node
pq->tail = node;
}
}
打包成节点的函数:
//创建队列节点
static QNode* CreatQNode(DataType x)
{
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
printf("malloc failed\n");
exit(-1);
}
newnode->x = x;//x赋值给节点
newnode->next = NULL;//下一个节点目前为NULL
}
static修饰使这个函数只能在当前文件被调用,这个函数的正常顺序应该放在入队的前面,这里便于分析讲解放在后面
出队
出队列主要操作:
1.判断队列所含节点情况
2.找到队列头节点的下一个,将头结点释放,让头指针指向原来头结点的下一个
这里的判断有点特殊需要判断:队列是否为空、队列含一个元素、队列含两个元素及以上
队列不能为空所以pq->head不能为NULL
队列含一个元素,出队后,你需要再对队列进行一定的处理
队列含两个元素及以上,头节点释放即可
//队头出
void PopQueue(Queue* pq)
{
assert(pq);
assert(pq->head);
//队列只有一个元素
if (pq->head->next==NULL)
{
free(pq->tail);
pq->head = pq->tail = NULL;
}
else
{
//队列两个元素及以上
//存储队头的下一个节点
QNode* cur = pq->head->next;
//释放队头
free(pq->head);
//队头指针指向原来队头的下一个节点
pq->head = cur;
}
}
有小伙伴可能会想为什么判断队列只有一个元素的时候不能用头指针和尾指针相等做判断条件,因为队列初始化时,头指针和尾指针相等,队列只有一个节点的时候二者还是相等的。
队列头节点和尾节点
//头节点
DataType QueueFront(Queue* pq)
{
assert(pq);
return pq->head->x;
}
//尾节点
DataType QueueBack(Queue* pq)
{
assert(pq);
return pq->tail->x;
}
两个简单作对比理解就行
队列长度
需要一个指针从头节点开始一直指向尾节点,过程中,每向后指向一次就计数一次
int QueueSize(Queue* pq)
{
assert(pq);
int count = 0;
QNode* cur = pq->head;
//cur从头指针一直到尾指针,循环最后一次,cur为尾指针,循环中cur被赋值为NULL,循环结束
while (cur)
{
cur = cur->next;//指向下一个节点
count++;
}
return count;
}
可以思考一下指针cur的循环过程,最后一次cur被赋值为什么
判断队空
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->head == NULL;
}
用这个函数可以简单打印队列的元素
int main()
{
Queue q;//创建队列结构体
InitQueue(&q);//初始化队列
PushQueue(&q, 2);//入队
PushQueue(&q, 4);
PushQueue(&q, 6);
PushQueue(&q, 8);
PushQueue(&q, 10);
printf("队列元素打印:");
while (!QueueEmpty(&q))
{
printf("%d ", QueueFront(&q));
PopQueue(&q);
}
printf("\n");
DestroyQueue(&q);//销毁队列
return 0;
}