10.1 栈和队列
一、栈(stack)(后进先出)
1、属性及操作
①top:指向最新插入的元素(起到的是下标作用)。
②empty:是否为空。
STACK-EMPTY
if S.top == 0
return TRUE
else return FALSE
③push:向栈内压入元素x。
PUSH(S,x)
S.top = S.top + 1
S[S.top] = x
④pop:弹出最上面(S.top)位置上的元素。
if STACK-EMPTY(S)
error "underflow"//栈下溢,即在试图对一个空栈执行弹出操作时
else S.top = S.top - 1
return S[S.top+1]
2、图解
(a)通过PUSH(S,17)\PUSH(S,3)变成(b)
(b)通过POP(S)变成(c)
二、队列(queue)(先进先出)
1、属性及操作
①head:指向队头元素。
②tail:指向队尾元素。
③ENQUEUE(Q,x):元素x入队操作,进入tail位置。
ENQUEUE(Q,x)
Q[Q.tail] = x //将x放在尾部上
if Q.tail ==Q.length //如果tail已经在尾部了,那么将tail指向队列第1位(像是一个环绕),供下一个元素放入
Q.tail = 1
else Q.tail = Q.tail + 1 //否则tail往后移动一位(供下一个元素放入)
④DEQUEUE(Q):head位置的元素出队操作
DEQUEUE(Q)
x = Q[Q.head] //x作为头部(head所指位置)元素出队
if Q.head = Q.length //同ENQUEUE的移动方式,也是类似一个环绕
Q.head = 1
else Q.head = Q.head + 1
return x
2、图解
(a)通过ENQUEUE(Q,17)将17放置在Q[12]的位置上,然后tail变成1
再通过ENQUEUE(Q,3)、ENQUEUE(Q,5)将3、5加入队列,最后tail=3。
(b)通过DEQUEUE(Q)弹出队头(head = 7的位置)15,然后head=8,最终新的队头关键字为6。
10.2 链表
一、最简单的单向链表(single linked list)(书上没讲,但我觉得有必要提)
1、单向链表基本知识
简单来讲,就是单向链表的每一个结点中包含data(数据域)和next(指针域),每一个结点(除了tail)的数据域用于存储数据,指针域用于存储下一个结点的地址(图中箭头就相当于一个指针),通过这种“箭头”我们将各个结点连接起来,从而形成链表。
2、单向链表各个操作的代码(C语言,推荐用VS运行,其中包含辅助指针思想)
(1) Linklist.h
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include<stdbool.h>
#define _CRT_SECURE_NO_WARNINGS
#pragma once//防止头文件重复
#ifdef __cplusplus
extern "C" {
#endif
//函数放在里面
//定义节点数据类型
struct LinkNode {
int data;
struct LinkNode* next;
};
//初始化链表
struct LinkNode* Init_LinkList();
//在值为oldval的后面添加一个新的数据newval
void InsertByValue_LinkList(struct LinkNode* header, int oldval, int newval);
//删除值为val的节点
void RemoveByValue_LinkList(struct LinkNode* header, int delValue);
//遍历
void Foreach_LinkList(struct LinkNode* header);
//销毁
void Destroy_LinkList(struct LinkNode* header);
//清空
void Clear_LinkList(struct LinkNode* header);
#ifdef __cplusplus
}
#endif
*(2)Linklist.c(最重要,内含各种基本操作)
#define _CRT_SECURE_NO_WARNINGS
#include "Linklist.h"
#include<stdio.h>
//初始化链表
struct LinkNode* Init_LinkList()
{
//struct LinkNode* newnode;
//拿到头结点,就相当于拿到整个链表
struct LinkNode* header = malloc(sizeof(struct LinkNode));
header->data = -1;
header->next = NULL;
int val = -1;
struct LinkNode* pRear;
pRear = header;//现在还指向头结点的首地址
while (true) {
printf("请输入一个值!\n");
scanf("%d", &val);
if (val == -1) {
break;
}
//创建一个新的节点
//newnode = (struct LinkNode*)malloc(sizeof(struct LinkNode));
struct LinkNode* newnode = malloc(sizeof(struct LinkNode));
newnode->data = val;
newnode->next = NULL;
//新节点插入到链表中
pRear->next = newnode;
//更新尾部指针指向
pRear = newnode;
}
//pRear->next = NULL;
return header;//返回头结点相当于把链表返回
};
//在值为oldval的位置插入一个新的数据newval
void InsertByValue_LinkList(struct LinkNode* header, int oldval, int newval)
{
if (header == NULL) {
return;
}
//法一
//struct LinkNode* pPrev = header;
//while (pPrev != NULL && pPrev->next->data != oldval) {
// pPrev = pPrev->next;
//}
创建新节点
//struct LinkNode* newnode = malloc(sizeof(struct LinkNode));
//newnode->data = newval;
//newnode->next = NULL;
新节点插入到链表中
//newnode->next = pPrev->next;
//pPrev->next = newnode;
//定义两个辅助指针变量(法二)
struct LinkNode* pCurrent = header->next;
struct LinkNode* pPrev = header;
struct LinkNode* pCurrent = pPrev->next;
while (pCurrent != NULL) {
if (pCurrent->data == oldval) {
break;
}
pPrev = pCurrent;
pCurrent = pCurrent->next;
}
//如果pCurrent为NULL,说明链表中不存在值为oldval的节点
if (pCurrent == NULL) {
return;
}
//创建新节点
struct LinkNode* newnode = malloc(sizeof(struct LinkNode));
newnode->data = newval;
newnode->next = NULL;
//新节点插入到链表中
newnode->next = pCurrent;
pPrev->next = newnode;
}
//删除值为val的节点
void RemoveByValue_LinkList(struct LinkNode* header, int delValue)
{
//添加辅助指针
struct LinkNode* pPrev = header;
struct LinkNode* pCurrent = pPrev->next;
if (header == NULL) {
return;
}
while (pCurrent != NULL) {
if (pCurrent->data == delValue) {
break;
}
pPrev = pCurrent;
pCurrent = pCurrent->next;
}
if (pCurrent == NULL) {
return;
}
//重新建立删除节点和待删除节点前驱和后继节点关系
pPrev->next = pCurrent->next;//让pprev的next指针指向删除节点的后一个
//释放删除节点内存
free(pCurrent);
pCurrent = NULL;
}
//遍历
void Foreach_LinkList(struct LinkNode* header)
{
if (header == NULL)
{
return;
}
//添加辅助指针变量
struct LinkNode* pCurrent;
pCurrent = header->next;//header不需要遍历
while (pCurrent != NULL) {
printf("%d ", pCurrent->data);
pCurrent = pCurrent->next;
}
}
//销毁
void Destroy_LinkList(struct LinkNode* header)
{
if (header == NULL)
{
return;
}
//建立辅助指针
struct LinkNode* pCurrent = header;
while (pCurrent != NULL) {
//先保存下一个节点的节点地址
struct LinkNode* pNext = pCurrent->next;
//释放当前节点内存
printf("%d节点被销毁。\n", pCurrent->data);
free(pCurrent);
//指针向后移动(重新生成pCurrent)
pCurrent = pNext;
}
}
//清空(只有头结点,然后next指向空)
void Clear_LinkList(struct LinkNode* header)
{
if (header == NULL)
{
return;
}
//定义辅助指针变量
struct LinkNode* pCurrent = header->next;
while (pCurrent != NULL) {
//先保存下一个节点的地址
struct LinkNode* pNext = pCurrent->next;
//释放当前节点内存
free(pCurrent);
//pCurrent指向下一个节点
pCurrent = pNext;
}
header->next = NULL;
}
(3)TestLinklist.c(用于测试)
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "Linklist.h"
void test() {
//初始化链表 100~600
struct LinkNode* header = Init_LinkList();
//打印链表
Foreach_LinkList(header);
//插入数据
InsertByValue_LinkList(header, 300, 666);
打印链表
printf("\n------\n");
Foreach_LinkList(header);
//删除节点
//printf("\n------\n");
//RemoveByValue_LinkList(header,300);
//Foreach_LinkList(header);
清空链表
//printf("\n------\n");
//Clear_LinkList(header);
//Foreach_LinkList(header);
//销毁链表
//Destroy_LinkList(header);
}
int main()
{
test();
system("pause");
return EXIT_SUCCESS;
}
二、更加复杂的链表
1、双向链表(double linked list)
(1)基本描述:
双向链表中每一个元素都是一个对象(结点),每一个对象有一个关键字key(data)和两个指针(域):next和prev,分别指向后继元素和前驱元素。如果x.prev = NIL,则元素x没有前驱,因此它是链表的头(head);如果x.next = NIL,同理,它是链表的尾(tail)。
属性L.head指向链表的头,如果L.head=NIL,那么链表为空。
(2)图解:
(a):表示双向链表{1,4,9,16},表头的prev和表尾的next都等于NIL,用斜杠表示,执行了LIST-INSERT(L,X)(x.key=25)变成了(b)中的新链表,且新链表以25为表头。
(b):调用LIST-DELETE(L,X)(x.key=4)后变成了(c)中新链表。
2、其他链表
1、未排序链表:元素以任何顺序出现;已排序链表:按照关键字的线性顺序排序。
2、循环链表:表头的prev指针指向表尾元素,表尾元素的next指针指向表头元素(前面的图加两个箭头)。
三、链表的操作(更加普适化的方法,推荐结合上面的C语言代码)
注意:伪代码里的x指的既是指针,也是结点内部的关键字内容,真实代码肯定要更复杂。
1、链表的搜索
LIST-SEARCH(L,k)
x = L.head
while x != NIL and x.key != k //遍历查找,如果x与k不相同则继续往后面的元素查找
x = x.next
return x
2、链表的插入(将x连接到链表前端,上面C语言代码中可以将x连接到链表中间)
LIST-INSERT(L,x)
x.next = L.head
if L.head != NIL
L.head.prev = x
L.head = x
x.prev = NIL
3、链表的删除
LIST-DELETE(L,x)
if x.prev != NIL
x.prev.next = x.next
else L.head = x.next
if x.head != NIL
x.next.prev = x.prev
4、哨兵(sentinel)(慎用)
(1)基本描述:
①哨兵是一个哑对象,作用是简化边界条件的处理。引入一个对象L.nil,将原来在代码中出现的NIL都换成对L.nil的引用。这样使双向链表成为有哨兵的双向链表。
②哨兵L.nil位于表头和表尾之间,L.nil.next指向表头,L.nil.prev指向表尾;同时表头的prev和表尾的next指向L.nil。
③因为L.nil.next指向表头,因此我们不再需要L.head。
(2)图解
(a):空链表。
(b):L.nil就如上面的基本描述所说,连接表头和表尾。
(c)~(d):插入删除的操作挺直观的,这里不再赘述。
(3)有哨兵后需要改变的方法
LIST-SEARCH'(L,k)
x = L.nil.next
while x != L.nil and x.key != k //遍历查找,如果x与k不相同则继续往后面的元素查找
x = x.next
return x
LIST-INSERT'(L,x)
x.next = L.nil.next
L.nil.next.prev = x
L.head.prev = x
L.nil.next = x
x.prev = L.nil
LIST-DELETE'(L,x)
x.prev.next = x.next
x.next.prev = x.prev
(4)慎用哨兵
如果是很短的链表,用哨兵会占用额外的存储空间造成存储浪费,我们只需在真正简化代码时才使用哨兵。