背景:非科班梳理基础知识ing
前言:数据结构与算法碎片积累(一)有60个小题(20节课),感觉看起来太多了,所以,接下来根据课程安排,采用课程内容分类方式来总结。这样子,或许回头看时候,更人性化一些。
20201124
1、啥叫双向链表?
答:简单理解,就是一个节点,由数据域data,有前指针prior,后指针next组成。
typedef char ElemType;
typedef struct DualNode {
ElemType data;
struct DualNode* prior;//前驱结点
struct DualNode* next;//后继结点
};
2、双向链表插入和删除操作是怎么样的?
答:
1)插入:
假设节点s插到节点p的前方,核心语句:
s->next=p;//s的后指针指向p
s->prior=p->prior;//s的前指针指向p的前一个结点
p->prior->next=s;//p的前一个结点的后指针指向s
p->prior=s;//p的前指针指向s
插入操作时候,一定要注意指针的指向以及先后顺序
2)删除:
假设节点p为待删除节点,关键语句:
p->prior->next=p->next;
p->next->prior=p->prior;
free(p);//释放删除节点所占的内存
关键就是把节点p的前节点的后指针指向p的后节点;p的后节点的前指针指向p的前结点,释放节点p内容。
3、双向链表相对于单链表来说,有啥特色?
答:
1)双向链表相对单链表而言,更复杂一些,每个节点多一个prior指针,对于插入和删除操作的顺序一定要注意;
2)双向链表可以有效提升算法的时间性能,也就是利用空间换取时间。
3)简单理解,双向链表有前后指向指针,指向更加灵活。
4、双向链表的应用实例演示。
答:
–要求实现用户输入一个数使得26个字母的排序发生变化,例如用户输入3,输出结果:
DEFGHIJKLMNOPQRSTUVWXYZABC
–同时需要支持负数,例如用户输入-3,输出结果:
XYZABCDEFGHIJKLMNOPQRSTUVW
//26字母输出,练习双向链表
#include<stdio.h>
#include<stdlib.h>
#define OK 1
#define ERROR 0
typedef char ElemType;
typedef int Status;
//双向链表
typedef struct DualNode {
ElemType data;
struct DualNode* prior;//前驱结点
struct DualNode* next;//后继结点
}DualNode, * DuLinkList;
//初始化一个双向链表,以及插入数据到双向链表中
Status InitList(DuLinkList* L)
{
DualNode* p, * q;//节点指针
int i;
*L = (DuLinkList)malloc(sizeof(DualNode));
if (!(*L))
{
return ERROR;
}//开辟失败,退出函数
(*L)->next = (*L)->prior = NULL;
p = *L;
for (i = 0; i < 26; i++)//依次将26个字母插入到链表中
{
q = (DualNode*)malloc(sizeof(DualNode));//开辟新结点
if (!q)
{
return ERROR;//开辟失败,退出
}
q->data = 'A' + i;//这种方式保存26个字母到双向链表里面
q->prior = p;
q->next = p->next;//注意,最开始的p->next都是NULL的,这一步
//保持了链表的最后一个结点的后指针都是指向NULL
//所以,这一步一定要注意,顺序不能和下一步调换
p->next = q;
p = q;//移动p到链表的最后面
if (i == 25)//下面注释步骤作用和这里是一样的
{
p->next = (*L)->next;
(*L)->next->prior = p;
}//这一步实现双向循环链表(注意,这个循环双向链表,不连接头结点*L的)
}
//p->next = (*L)->next;
//(*L)->next->prior = p;//实现最后结点和字符A结点链接(注意不是头结点*L)
return OK;
}
//这函数作用是控制表头*L指针的移动步数,从而达到输出字母的顺序要求
void Caesar(DuLinkList* L, int i)
{
if (i > 0)
{
do {
(*L) = (*L)->next;//这里决定了后移
} while (--i);//由于i大于0,所以要减少来控制后移步数
}
if (i < 0)
{
do {
(*L) = (*L)->prior;//这里决定了前移
} while (++i);//由于i小于0,所以要增加来控制后移步数
}
}
int main()
{
DuLinkList L;
int i,n;
InitList(&L);
printf("请输入一个整数:");
scanf_s("%d", &n);//这里由于在vs上面运行的,提示了scanf不安全
//需要写成scanf_s类型
Caesar(&L, n);
for (i = 0; i < 26; i++)
{
L = L->next;
printf("%c", L->data);
}
return 0;
}
5、栈是啥?
答:可以这么理解:
1)栈是一种线性表;
2)栈是一种先进后出(如子弹入弹夹)的数据结构;
3)栈的删除或插入操作只是在表尾(又称栈顶top)进行;
4)栈的表尾,又称栈顶(top);表头,又称为栈底(bottom)。
5)栈的插入操作(push),称为进栈,也称为压栈;删除操作(pop),叫出栈,又称为弹栈;
6)栈有两种存储方式,顺序存储结构和链式存储结构。
6、栈的常规操作,结构定义,入栈,出栈,清空栈,销毁栈是哈?
答:案例展示,将一个二进制数转为十进制数:
//利用栈的数据结构特点,将二进制转换为十进制数
#include<stdio.h>
#include<stdlib.h>
#include<math.h>
#define STACK_INIT_SIZE 20
#define STACKINCREMENT 10
//栈结构定义
typedef char ElemType;
typedef struct {
ElemType* top;//栈顶
ElemType* base;//栈底
int stackSize;//栈空间大小
}sqStack;
void InitStack(sqStack* s) {
s->base = (ElemType*)malloc(STACK_INIT_SIZE * sizeof(ElemType));//开辟空间
if (!s->base)
return;//开辟失败则退出函数
s->top = s->base;//刚开始栈顶栈底指向相同
s->stackSize = STACK_INIT_SIZE;//初始化栈空间大小
}//初始化一个栈
//入栈操作
void Push(sqStack* s, ElemType e)
{
if (s->top - s->base >= s->stackSize)//检测栈是否已经满了
{
s->base = (ElemType*)realloc(s->base, (s->stackSize + STACKINCREMENT)*sizeof(ElemType));
//复制原来的数据,并开辟到更大的内存中
if (!s->base)
return;//开辟失败的话,退出函数
s->stackSize = s->stackSize + STACKINCREMENT;//栈容量不跟新,会导致程序崩溃(qt运行倒不会)
}
*(s->top) = e;//插入元素入栈
s->top++;//栈顶上移
}
//出栈操作
void Pop(sqStack* s, ElemType* e)
{
if (s->top == s->base)
return;//判断是否出现下溢
*e = *--(s->top);//先栈顶元素下移,然后,返回出栈元素
}
//清空栈操作
void ClearStack(sqStack* s)
{
s->top = s->base;
}//将栈顶指针指向栈底,就不再读取栈数据,达到清空目的(清空不表示数据删除;销毁才是将数据彻底销毁)
//销毁一个栈
void DestoryStack(sqStack* s)
{
int i, len;
len = s->stackSize;
for (i = 0; i < len; i++)
{
//ElemType* p;
//p = s->base;
//s->base++;
//free(p);
free(s->base);
s->base++;
}//这里存在一个bug,还没有解决20201124
s->base = s->top = NULL;
s->stackSize = 0;
}//从栈底不断上移,并释放内存
int StackLen(sqStack s)//对比上面的可以发现,对应需要修改的,传指针;
//不需要修改数据的,只是传值
{
return(s.top - s.base);
}
int main()
{
ElemType c;
sqStack s;
InitStack(&s);//初始化栈
int len, i, sum = 0;
printf("请输入二进制数,输入#符号表示结束!\n");
scanf_s("%c", &c);
while (c != '#')
{
Push(&s, c);
scanf_s("%c", &c);//这里的c是下一个入栈的字符
//printf("%c\n", c);
}
getchar();//回车键,产生的字符'\n'(==10),该函数可避免
//该字符一直在缓冲区(下次输入操作时候,10会造成程序混乱)
//其作用就是将'\n'从缓冲区去掉
len = StackLen(s);
printf("栈的当前容量是:%d\n", len);
for (i = 0; i < len; i++)
{
Pop(&s,&c);
sum = sum+(c - 48) * pow(2, i);//pow就是被调用的数学次方函数
}
printf("转换为十进制数是:%d\n", sum);
ClearStack(&s);
int k = StackLen(s);
if (k == 0) {
printf("\n清空成功!\n");
}
//DestoryStack(&s);//销毁栈还有bug,后面修正了再更新,销毁思路应该没问题的
return 0;
}
7、栈的链式存储结构插入和删除的操作注意点?
答:
栈因为只是栈顶用来做插入和删除操作,所以比较好的方法就是将栈顶放在单链表的头部,栈顶指针和单链表指针合二为一。栈的链式存储结构图示可如:
typedef struct StackNode {
ElemType data;//存放栈的数据
struct StackNode* next;
}StackNode,*LinkStackPtr;//栈节点,链栈节点指针
typedef struct LinkStack {
LinkStackPtr top;//top指针
int count; //栈元素计算器
};
//进栈操作
//对于栈链的Push操作,假设元素值为e的新节点是s,top为栈顶指针,那么
Status Push(LinkStack* s, ElemType e)
{
LinkStackPtr p = (LinkStackPtr)malloc(sizeof(StackNode));//开辟栈节点
p->data = e;//保存需要插入的元素
p->next = s->top;//把栈顶指针保存
s->top = p;//把栈点放到原来栈顶位置,同时栈顶指针上移
s->count++;//栈的容量加1
return OK;
}
//出栈操作
Status Pop(LinkStack* s, ElemType* e) {
LinkStackPtr p;
if (StackEmpty(*s))//StackEmpty判断是否为空栈,空的话返回1
return ERROR;
*e = s->top->data;//保存删除的元素
p = s->top;//记录栈顶地址
s->top = s->top->next;//栈顶指针下移
free(p);//释放原来栈顶内存
s->count--;//栈的容量减1
return OK;
}
8、链式栈的应用,逆波兰计算器怎么实现?
答:
1)正常表达式->逆波兰表达式的感性认识:
a+b - > a b +
a+(b-c) -> a b c - +
a+(b-c)d -> a b c – d * +
a+d(b-c) -> a d b c - * +
2)代码实现,就是通过逆波兰表达式来计算结果:
//逆波兰计算器
#include<stdio.h>
#include<ctype.h>
#include<stdlib.h>
#define STACK_INIT_SIZE 20
#define STACKINCREMENT 10
#define MAXBUFFER 10
typedef double Elemtype;
typedef struct {
Elemtype* base;
Elemtype* top;
int stackSize;
}aqStack;
void InitStack(aqStack *s) {
s->base = (Elemtype*)malloc(STACK_INIT_SIZE * sizeof(Elemtype));
if (!s->base)
return;
s->top = s->base;
s->stackSize = STACK_INIT_SIZE;
}
void Push(aqStack* s, Elemtype e)
{
//栈满,追加空间
if (s->top - s->base >= s->stackSize) {
s->base = (Elemtype*)realloc(s->base, (s->stackSize + STACKINCREMENT) * sizeof(Elemtype));
if (!s->base)
return;
s->top = s->base + s->stackSize;
s->stackSize = s->stackSize + STACKINCREMENT;
}
*(s->top) = e;
s->top++;
}
void Pop(aqStack* s, Elemtype* e)
{
if (s->top == s->base)
return;
*e = *--(s->top);//将栈顶元素弹出并修改栈顶指针
}//出栈
int StackLen(aqStack s)
{
return(s.top - s.base);
}
int main() {
aqStack s;//实例化一个栈对象
char c;//保存输入的字符
double d, e;//用于接收出栈入栈元素
char str[MAXBUFFER];//小数的缓冲器
int i=0;
InitStack(&s);//初始化栈
printf("请按逆波兰表达式输入待计算数据,数据间、数据与运算符间,运算符间之间用空格隔开,以#号作为结束标志:\n");
scanf_s("%c", &c);
while (c!='#') {//这里注意的是,这里的c还是一个一个字符读取进来的
while (isdigit(c)||c=='.') {//用于过滤数字
//该while循环,在不碰到空格前,会继续遍历c下一个字符
//isdigit()属于ctype库函数中,检测是十进制数字符(0~9)返回非零,否则返回0
str[i++] = c;//保存加载进来的字符
str[i] = '\0';//'\0'在字符串中表示结束意思,必须加上这个,否则调试会错误
//通过测试发现,如果不加这个结束符,会导致上一个字符段的保
//存的字符会继续存在并使用,这里原因是,i=0的作用是重新从数组的
//第一个开始重新赋值保存,所以每次都要添加结束符,保证不会读取到
//原来遗留在数组中的数据
if (i >= 10) {
printf("出错:输入单个数据过大!\n");
return -1;
}
scanf_s("%c", &c);//驱使继续读取c下一个字符
if (c == ' ')//如果碰到空格
{
d = atof(str);//atof()函数是将保存在str数组里面的字符串转换为浮点型数据
Push(&s, d);//转化的浮点数入栈
i = 0;//为下一次c字符组成的串转换为浮点数做准备
printf("%f\n", d);//测试获取得到的浮点数
break;//第一个参数入栈成功就跳出该循环,进入下一个字符c的读取
}
}
switch (c) {
case '+':
Pop(&s,&e);
Pop(&s, &d);
Push(&s, d + e);//为啥这里可以使用d,e呢?很简单,因为,有了出栈带来的参数
break;
case'-':
Pop(&s, &e);
Pop(&s, &d);
Push(&s, d - e);
break;
case'*':
Pop(&s, &e);
Pop(&s, &d);
Push(&s, d * e);
break;
case'/':
Pop(&s, &e);
Pop(&s, &d);
if (e != 0) {
Push(&s, d / e);
}
else {
printf("\n出错:除数为零!");
return -1;
}
break;
}
scanf_s("%c", &c);//驱使继续读取c下一个字符,而且在上一个内在的while循环内读取过的c字符是不会再重复读取的,
//而是继续读取,这里感觉好像使得c在循环里面成为全局变量,遍历过的就接着下一个遍历
}
Pop(&s,&d);
printf("\n最终的计算结果为:%f\n", d);
return 0;
}
9、如何实现从中缀表达式转换为后缀表达式?
答:
1)感性认识,中缀表达式->后缀表达式
(1-2)*(4+5)->1 2 – 4 5 + *
2)逆波兰表达式(RPN),是不需要括号的后缀表达式
3)相对于中缀表达式(人比较容易理解),后缀表达式更适合计算机,因为,后缀运算时候,计算机不需不断判断。
4)中缀转后缀代码实现:
//将中缀表达式转为后缀表达式
#include<stdio.h>
#include<stdlib.h>
#include<math.h>
#define STACK_INIT_SIZE 20
#define STACKINCREMENT 10
typedef char ElemType;
typedef struct {
ElemType* top;
ElemType* base;
int stackSize;
}sqStack;//栈
void InitStack(sqStack* s) {
s->base = (ElemType*)malloc(STACK_INIT_SIZE * sizeof(ElemType));
if (!s->base)
return;
s->top = s->base;
s->stackSize = STACK_INIT_SIZE;
}//初始化一个栈
void Push(sqStack* s, ElemType e)
{
if (s->top - s->base >= s->stackSize)//检测栈是否已经满了
{
s->base = (ElemType*)realloc(s->base, (s->stackSize + STACKINCREMENT) * sizeof(ElemType));
//复制原来的数据,并开辟到更到的内存中
if (!s->base)
return;
s->stackSize = s->stackSize + STACKINCREMENT;//栈容量不跟新,会导致程序崩溃(qt运行倒不会)
}
*(s->top) = e;
s->top++;
}//入栈操作
void Pop(sqStack* s, ElemType* e)
{
if (s->top == s->base)
return;//判断是否出现下溢
*e = *--(s->top);
}//出栈操作
int StackLen(sqStack s)//对比上面的可以发现,对应需要修改的,传指针;
//不需要修改数据的,只是传值
{
return(s.top - s.base);
}
int main() {
sqStack s;
char c, e;
InitStack(&s);
printf("请输入中缀表达式,以#作为结束标志:");
scanf_s("%c", &c);
while (c!='#')
{
while(c >= '0' && c <= '9')
{
printf("%c", c);
scanf_s("%c", &c);//这里使用这个,可以会出现有#情况,但是,还有
//没有回到最外面的while判断#,所以需要在下面的
//输入类型中添加#判断,如果有,那么跳出循环
if (c < '0' || c>'9') {
printf(" ");
}//这样处理是为了,大于9的数值输入的,如12,显示时候,不分开
}
//数字判断,如果是数字就直接打印
if(']'==c)//这里和 c==']' 意义一样的,不过,这里写少一个= ,
//使得变成了赋值语句,编译器是检测不出错误的;而写成
//变量在右侧,少写=会提醒报错的。(常量在左侧,赋值
//操作会报错)
{
Pop(&s, &e);//出栈
while ('['!=e)
{
printf("%c ", e);//打印出栈元素
Pop(&s, &e);//继续出栈出栈
}//如果没有遇到左括号,一直打印
}
//当输入元素为右括号时候,需要把括号中运算符号出栈,并且把里面+、-、*、/ 打印出来
//这模块的意思,就是把括号优先级提升最高,把里面的运算符打印出来,但是括号只是出栈,不用打印
else if ('+'==c||c=='-')
{
if (!StackLen(s))//如果栈为空
{
Push(&s, c);//入栈
}//如果栈为空,把符号元素保存到栈中
else {
do {
Pop(&s, &e);//出栈,检测栈顶元素
if ('[' == e) {
Push(&s, e);
}//如果是左括号,则左括号回栈
else
{
printf("%c ",e);
}//出栈的元素不是左括号,则打印
} while (StackLen(s)&&e!='[');//栈不为空并且出栈元素不为左括号都要进行
Push(&s, c);
}//栈不为空时,输入的+或者-则跟栈顶元素比较,没遇到左括号,都要出栈打印;
//遇到左括号,就把运算符入栈
}
//这模块就是判断输入+或者-时,分两种情况,首先判断栈是否为空,不为空则入栈;栈不为空,那么输入元素
//需要和栈顶元素比较,这时候,也分两种情况,如果此时栈顶元素是左括号,那么,就把元素入栈;否则,就
//一直打印出栈
//这里,相当于把之前入栈的运算符+、-(不包含括号内的,上一模块已打印完毕),全部打印出栈
else if (c == '*' || c == '/' || c == '[') {
Push(&s, c);
}
//如果是*、/、[ 时候,那么就把这几个符号入栈
else if (c == '#') {
break;
}//这里判断第一个while有可能出现#情况,然后跳出循环
//当进行多数字(如,1000,就是4个字符组成的)判断时候,存在一个内循环,如果此时已经碰到了#,不加处理
//,下面又有一个scanf_s(继续读取下一个),那么就会使得读取到一些未知区域,会导致出错。
else
{
printf("\n输入错误!\n");
return -1;
}
//如果输入的是其他信息,那么就提示输入错误
scanf_s("%c", &c);//继续读取下一个字符
}//当未读取到#符号时候,就一直循环判断,看到底是打印还是入栈,还是出栈
while (StackLen(s))
{
Pop(&s, &e);
printf("%c ", e);
}//当上面的大循环结束了,栈中还保留运算符的话,那么依次从栈顶出栈并打印
return 0;
}
//注意,原来采用的是()括号的,后面发现,调试窗口自动为中文输入法,导致
//容易即使输入方式为中文输入也看不出来,所以,采用[]代替()
//虽然,通过分步调试理清楚了每一步的作用,但是,内在的算法思路还是没有很清晰的层次样式
10、队列有啥特性?
答:
1)队列,只能一端插入,另一端删除的线性表;
2)队列,是一种先进先出的线性表(注意与先进后出的栈区分)
3)队列,可用顺序表实现,也可以用链表实现;但,一般情况下,使用链表来实现,所以,又可简称为链队列。
4)队列的结构特性:
空队列时,front和rear都指向头结点
11、队列全家桶(结构、入队、出队、销毁操作)是啥?
答:案例展示:
#include <malloc.h>
#include<stdio.h>
#include <stdlib.h>
//链队列的结构代码
#define ElemType char
typedef struct QNode {
ElemType data;
struct QNode* next;
}QNode,*QueuePrt;//节点结构
typedef struct
{
QueuePrt front, rear;//队头、队尾指针
}LinkQueue;
//创建或者初始化一个队列
void initQueue(LinkQueue* q) {
q->front = q->rear = (QueuePrt)malloc(sizeof(QNode));
if (!q->front)
exit(0);//这是一个函数,而return是一个关键字
q->front->next = NULL;
}
//入队操作
void InsertQueue(LinkQueue* q, ElemType e) {
QueuePrt p;
p = (QueuePrt)malloc(sizeof(QNode));
if (p == NULL)
exit(0);
p->data=e;
p->next = NULL;
q->rear->next = p;//q的队尾由原来指向null,重新指向新加入的p
q->rear = p;//将队列q的队尾指针重新指向最后一个节点
}
//出队操作
void DeleteQueue(LinkQueue* q, ElemType* e) {
QueuePrt p;
if (q->front == q->rear)//空队列情况
return;
//非空队列情况
p = q->front->next;
*e = p->data;
q->front->next = p->next;
if (q->rear == p)//队列只一个元素,而元素被出队了;在释放该空间前
//将该尾指针移动回来,和头指针指向同一节点头结点
q->rear = q->front;
free(p);//释放删除节点所占内存
}
//销毁队列
int DestoryQueue(LinkQueue* q) {
if (1) {
while (q->front) {
q->rear = q->front->next;
free(q->front);
q->front = q->rear;
}
return 1;
}
}
//创建一个链表并初始化,入队操作,出队操作并销毁
int main() {
LinkQueue s;
initQueue(&s);
char e,t;
int i = 0;
printf("请输入字符并以#结尾:\n");
scanf_s("%c", &e);
while (e != '#') {
InsertQueue(&s, e);
i++;
scanf_s("%c", &e);
}
while (i--)
{
DeleteQueue(&s, &t);
printf("%c ", t);
}
int a=DestoryQueue(&s);
if (a == 1) {
printf("\n销毁完毕\n");
}
system("pause");
return 0;
}
20201210
12、队列的顺序存储结构之循环队列
答:
1)队列只是采用顺序存储结构时,会导致出队时候,时间复杂度达到O(n)【队头不动情况】,或者假溢出的情况【队头移动情况】出现,所以为了解决这个问题,提出一种循环队列的顺序存储队列。
2)循环队列定义:
队列容量是固定的,并且队头和队尾指针都是可以随着元素入队而发生改变,逻辑上,循环队列如同一个环形存储空间(只是用顺序表模拟出的逻辑上的循环,实际上是不存在的)
3)但是,为避免循环时,队头队尾指针重新相遇情况,我们在入队或者出队时候,对其进行取模运算:
(rear+1)%QueueSize
(front+1)%QueueSize
因此,队列的顺序存储结构存在各种麻烦,一般情况下,队列采用的是链式存储结构的。
但是,循环队列基本操作还是要会的:
//定义一个循环队列
#define MAXSIZE 100
#define ElemType char
typedef struct {
ElemType* base;//用于存放内存分配基地址(我理解就是,指向一个循环顺序队列,然后,
//又可以指向里面的任意位置,根据[]来访问具体位置),也可以用数组存放基地址
int front;
int rear;
}cycleQueue;
//初始化一个循环队列
void initQueue(cycleQueue* q) {
q->base = (ElemType*)malloc(MAXSIZE * sizeof(ElemType));//开辟的是一个循环顺序队列整体内容空间,并q->base指向它
if (!q->base)
exit(0);
q->front = q->rear = 0;//队头队尾指向初始化都在0号位置
}
//入队操作(队尾插入)
void InsertQueue(cycleQueue* q, ElemType e) {
if ((q->rear + 1) % MAXSIZE == q->front)//这里判断元素是否已经满了
return;//队列已满
q->base[q->rear] = e;//数组和指针相互引用;把插入的元素放到队列相应的位置,根据[]来确定
q->rear = (q->rear + 1) % MAXSIZE;//指向下一个顺序表元素编号
}
//出队操作(队头出队)
void DeleteQueue(cycleQueue* q, ElemType* e) {
if (q->front == q->rear)
return;//队列为空
*e = q->base[q->front];//返回出队元素
q->front = (q->front + 1) % MAXSIZE;//队头指向下一个顺序表元素编号
}
#########################
不积硅步,无以至千里
好记性不如烂笔头
盗图归授课老师所有,致谢