郝斌老师讲的《数据结构与算法》课程通俗易懂,在b站人气很高!视频链接为:
https://b23.tv/med0e3
若本笔记有看不懂的地方,请去看上面链接的视频,很方便地检索到你想到看的知识点。
以下为我的数据结构与算法总结
目录
一、数据结构概述
1.1数据结构定义
学完数据结构之后会对面向过程的函数有一个更深的了解。
我们如何把现实中大量而复杂的问题以特定的数据类型和特定的存储结构(个体的关系) 保存到主存储器(内存)中,以及在此基础上为实现某个功能(比如查找某个元素,删除某个元素,对所有元素进行排序)而执行的相应操作,这个相应的操作也叫算法。
(比如班里有15个人,其信息量也许一个数组就搞定了,但是假如10000个,怎么办?内存也许没有这么多连续的空间,所以我们改用链表,you see这就是与存储有关系。又比如,人事管理系统的信息存储, 因为存在着上下级的关系,所以数组和链表就无能为力了,这时候我们用树,再比如我们做的是交通图,站和站之间肯定要连通,这 时候以上的存储方式又无能为力了,所以我们又有了图。图就是每个结点都可以和其他结点产生联系。所以当我们要解决 问题时,首先要解决的是如何把这些问题转换成数据,先保存到我们的主存中,)
数据结构 = 个体的存储 + 个体的关系存储
算法 = 对存储数据的操作
算法定义:通俗的说算法是解题的方法和步骤
衡量算法的标准
时间复杂度:程序大概要执行的次数,而非执行的时间;
空间复杂度:程序执行过程中大概所占用的最大内存空间;
难易程度:用易懂,避免过于复杂;
健壮性。
数据结构的地位:数据结构是软件中最核心的课程
程序 = 数据的存储 + 数据的操作 + 可以被计算机执行的语言
二、预备知识(C语言的复习)
2.1 指针
指针的重要性:(内存是可以被CPU直接访问的,硬盘不行;
主要靠地址总线,数据总线,控制总线。)
指针是C语言的灵魂。
地址:内存单元的编号,从0开始的非负整数。范围0 ~ FFFFFFFF(即0 ~ 4G-1)
指针就是地址,地址就是指针;
指针变量是存放内存单元地址的变量;
指针的本质是一个操作受限的非负整数。
指针的分类:
①基本类型的指针;
②指针和数组的关系
指针的具体知识这里不再赘述。想要复习相关知识请去看郝斌老师的笔记:https://blog.csdn.net/weixin_45519751/article/details/107869403
2.2 结构体
为什么会出现结构体
为了表示一些复杂的数据,而普通的基本类型变量无法满足要求
什么叫结构体
结构体是用户根据实际需要自己定义的复合数据类型
如何使用结构体
两种方式:
struct Student st = {
1000, "zhangsan", 20}
struct Student * pst = &st;
方式1.
st.sid
方式2.
pst->sid
pst所指向的结构体变量中的sid这个成员
我们一般使用方式2,因为指针在参数传递过程中占用的内存少(4个字节),程序运行效率高。
注意事项:
结构体变量不能加减乘除,但可以相互赋值;
普通结构体变量和结构体指针变量作为函数参数的传递(结构体指针变量传参效率更高);
结构体知识请去看郝斌老师的笔记:
https://blog.csdn.net/weixin_45519751/article/details/107869403
2.3 动态内存的分配和释放(malloc())
(动态分配的内存一定要手动释放,否则造成内存泄露。)
想要复习相关知识请去看郝斌老师的笔记:https://blog.csdn.net/weixin_45519751/article/details/107869403
2.4 关于指针、结构体、动态内存、typedef的例子
例子1:
# include <stdio.h>
# include <malloc.h>
int main(void)
{
int a[5] = {
4, 10, 2, 8, 6};
int len;
printf("请输入你需要分配的数组的长度: len = ");
scanf("%d", &len);
int * pArr = (int *)malloc(sizeof(int) * len);
// *pArr = 4; //类似于 a[0] = 4;
// pArr[1] = 10; //类似于a[1] = 10;
// printf("%d %d\n", *pArr, pArr[1]);
//我们可以把pArr当做一个普通数组来使用
for (int i=0; i<len; ++i)
scanf("%d", &pArr[i]);
for (i=0; i<len; ++i)
printf("%d\n", *(pArr+i));
free(pArr); //把pArr所代表的动态分配的20个字节的内存释放
return 0;
}
例子2:跨函数使用内存
# include <stdio.h>
# include <malloc.h>
struct Student
{
int sid;
int age;
};
struct Student * CreateStudent(void);
void ShowStudent(struct Student *);
int main(void)
{
struct Student * ps;
ps = CreateStudent();
ShowStudent(ps);
return 0;
}
void ShowStudent(struct Student * pst)
{
printf("%d %d\n", pst->sid, pst->age);
}
struct Student * CreateStudent(void)
{
struct Student * p = (struct Student *)malloc(sizeof(struct Student));
p->sid = 99;
p->age = 88;
return p;
}
例子3:typedef函数的使用
typedef int INT; // 相当于给int起了一个别名 INT
typedef struct Student
{
int sid;
char name[100];
char sex;
} ST; //ST st 就相当于 struct Student st,给struct Student 起了别名ST,这样简洁了代码
typedef struct Student
{
int sid;
char name[100];
char sex;
} * ST; //ST就相当于struct Student *
若不懂typedef则请去看郝斌老师的笔记:
https://blog.csdn.net/weixin_45519751/article/details/107883393
三、线性结构
线性结构即把所有节点用一条直线串起来。
3.1 连续存储【数组】
什么叫数组:元素类型相同,大小相等。
数组的优缺点
优点:存取速度很快
缺点:插入删除元素很慢(因为,你要是在数组中间插入一个值的话,需要把此值要插入到位置的后面的所有元素向后移。显然,这样导致速度很慢)
数组例子:
我们想自己实现数组的功能,即我们写一个结构体,里面有数组首地址、当前数组内已有元素的长度(有效长度)、数组长度,然后编写一些函数实现对数组的操作,分别为:初始化数组、获取某个位置上的元素、判断数组是否已经满了、判断数组是否为空、打印数组内所有元素、向数组中追加元素、向数组某个位置插入元素、排序(本例用的是选择排序)。
# include <stdio.h>
# include <malloc.h> //包含了malloc函数
# include <stdlib.h> //包含了exit函数
//定义了一个数据类型,该数据类型的名字叫做struct Arr, 该数据类型含有三个成员,分别是pBase, len, cnt
struct Arr
{
int * pBase; //存储的是数组第一个元素的地址
int len; //数组所能容纳的最大元素的个数(数组有效长度)
int cnt; //当前数组有效元素的个数
};
//初始化数组
void init_arr(struct Arr * pArr,int length);//分号不能省
//获取某个位置上的元素
int get(int pos);
//判断数组是否已经满了
bool is_full(struct Arr * pArr);
//判断数组是否为空
bool is_empty(struct Arr * pArr);
//打印数组内所有元素
void show_arr(struct Arr * pArr);
//向数组中追加元素
void append_arr(struct Arr * pArr,int val); // pos的值从1开始
//向数组某个位置插入元素
void insert_arr(struct Arr * pArr,int pos,int val);
//排序(本例用的是选择排序)
void sort_arr (struct Arr * pArr);
//倒置数组
void inversion_arr(struct Arr * pArr);
int main(void)
{
struct Arr arr;
int val;
init_arr(&arr, 6);
show_arr(&arr);
append_arr(&arr, 1);
append_arr(&arr, 10);
append_arr(&arr, -3);
append_arr(&arr, 6);
append_arr(&arr, 88);
append_arr(&arr, 11);
if ( delete_arr(&arr, 4, &val) )
{
printf("删除成功!\n");
printf("您删除的元素是: %d\n", val);
}
else
{
printf("删除失败!\n");
}
/* append_arr(&arr, 2);
append_arr(&arr, 3);
append_arr(&arr, 4);
append_arr(&arr, 5);
insert_arr(&arr, -1, 99);
append_arr(&arr, 6);
append_arr(&arr, 7);
if ( append_arr(&arr, 8) )
{
printf("追加成功\n");
}
else
{
printf("追加失败!\n");
}
*/
show_arr(&arr);
inversion_arr(&arr);
printf("倒置之后的数组内容是:\n");
show_arr(&arr);
sort_arr(&arr);
show_arr(&arr);
//printf("%d\n", arr.len);
return 0;
}
void init_arr(struct Arr * pArr, int length)
{
pArr->pBase = (int *)malloc(sizeof(int) * length);
if (NULL == pArr->pBase)
{
printf("动态内存分配失败!\n");
exit(-1); //终止整个程序
}
else
{
pArr->len = length;
pArr->cnt = 0;
}
return;
}
bool is_empty(struct Arr * pArr)
{
if (0 == pArr->cnt)
return true;
else
return false;
}
bool is_full(struct Arr * pArr)
{
if (pArr->cnt == pArr->len)
return true;
else
return false;
}
void show_arr(struct Arr * pArr)
{
if ( is_empty(pArr) )
{
printf("数组为空!\n");
}
else
{
for (int i=0; i<pArr->cnt; ++i)
printf("%d ", pArr->pBase[i]); //int *
printf("\n");
}
}
bool append_arr(struct Arr * pArr, int val)
{
//满是返回false
if ( is_full(pArr) )
return false;
//不满时追加
pArr->pBase[pArr->cnt] = val;
(pArr->cnt)++;
return true;
}
bool insert_arr(struct Arr * pArr, int pos, int val)
{
int i;
if (is_full(pArr))
return false;
if (pos<1 || pos>pArr->cnt+1) //
return false;
for (i=pArr->cnt-1; i>=pos-1; --i)
{
pArr->pBase[i+1] = pArr->pBase[i];
}
pArr->pBase[pos-1] = val;
(pArr->cnt)++;
return true;
}
bool delete_arr(struct Arr * pArr, int pos, int * pVal)
{
int i;
if ( is_empty(pArr) )
return false;
if (pos<1 || pos>pArr->cnt)
return false;
*pVal = pArr->pBase[pos-1];
for (i=pos; i<pArr->cnt; ++i)
{
pArr->pBase[i-1] = pArr->pBase[i];
}
pArr->cnt--;
return true;
}
void inversion_arr(struct Arr * pArr)
{
int i = 0;
int j = pArr->cnt-1;
int t;
while (i < j)
{
t = pArr->pBase[i];
pArr->pBase[i] = pArr->pBase[j];
pArr->pBase[j] = t;
++i;
--j;
}
return;
}
void sort_arr(struct Arr * pArr)
{
int i, j, t;
for (i=0; i<pArr->cnt; ++i)
{
for (j=i+1; j<pArr->cnt; ++j)
{
if (pArr->pBase[i] > pArr->pBase[j])
{
t = pArr->pBase[i];
pArr->pBase[i] = pArr->pBase[j];
pArr->pBase[j] = t;
}
}
}
}
3.2 离散结构【链表】
链表定义:
n个节点离散分配;
彼此通过指针相连;
每个节点只有一个前驱节点,每个节点只有一个后续节点;
首节点没有前驱节点,尾节点没有后续节点。
链表的专业术语:
首节点:
第一个有效节点
尾节点:
最后一个有效节点
头节点:
头结点的数据类型和首节点的类型一样;
其没有存放有效数据,其是在最最前面的,是在首节点之前的;
其主要是为了方便对链表的操作。
头指针:(指向头节点)
其是指向头节点的指针变量
尾指针:
指向尾节点的指针
链表的优缺点:
优点:
空间没有限制
插入删除元素很快
缺点:
存取速度很慢。
而数组插入删除元素就很慢,这是因为,假如其在某一位置插入某一个元素,就需要把这一
位置之后的元素每一个都 后移,导致速度就慢了下来。
(头结点有可能很大,占的内存可能大,假设我想造一个函数输出所有链表的值,那你如果不用头指针类型做形参,那由于不同链表的头节点不一样大小,这样就没办法找出形参。指针4个字节,因此我们根据头指针确定一个链表)
确定一个链表需要几个参数?答:只需要一个头指针参数,因为我们通过头指针可以推算出链表的其他所有信息
链表举例:
下面程序中的struct Node{}就是我们自己创建的节点类型。
# include <stdio.h>
typedef struct Node
{
int data;//数据域
struct Node * PNext;//指针域
} NODE, *PNODE;//NODE等价于struct Node,PNODE等价于struct Node *
int main(void)
{
return 0;
}
链表分类:
单链表:每一个节点只有一个指针域 。
双链表:每一个节点有两个指针域。
循环链表:能通过任何一个节点找到其他所有的节点 。
非循环链表:不能通过任何一个节点找到其他所有的节点。
注:循环链表属于双链表的一种特殊形式,即循环链表是双链表的一个子集。
优缺点:
优点:空间没有限制;插入和删除元素很快。
缺点:存取速度很慢。
算法
算法:
狭义的算法是与数据的存储方式密切相关
广义的算法是与数据的存储方式无关
泛型:利用某种技术达到的效果,就是:不同的存储方式,执行的操作是一样的。(给你一种假象,只不过牛人从内部
都弄好了)。
算法包括:
遍历
查找
清空
销毁
求长度
排序
删除节点
插入节点
学习算法的方法:
很多算法我们大多数人根本解决不了!!!!!!因为很多都属于数学上的东西,所以我们把答案找出来,如果能
把算法看懂就行,但是大部分人又看不懂,如何看懂呢?分三步:流程,语句,试数。
这个过程肯定会不断地出错,所以不断出错,不断改错,这样反复敲很多次,才能有个提高。实在看不懂就先背会。
注意:java中变成垃圾内存则会自动释放,但是C和C++则不会,所以要手动释放,否则会引起内存泄露。delete(C++中的)等于free(C中的)。
代码实现
即我们自己创建一个单链、 非循环链表。
# include <stdio.h>
# include <malloc.h>
# include <stdlib.h>
typedef struct Node
{
int data; //数据域
struct Node * pNext; //指针域
}NODE, *PNODE; //NODE等价于struct Node PNODE等价于struct Node *
//函数声明
PNODE create_list(void); //创建链表
void traverse_list(PNODE pHead); //遍历链表
bool is_empty(PNODE pHead); //判断链表是否为空
int length_list(PNODE); //求链表长度
bool insert_list(PNODE pHead, int pos, int val); /*在pHead所指向链表的
第pos个节点的前面插入一个新的结点,该节点的值是val, 并且pos的值是从1开始*/
bool delete_list(PNODE pHead, int pos, int * pVal); /*删除链表第pos个节
点,并将删除的结点的值存入pVal所指向的变量中, 并且pos的值是从1开始*/
void sort_list(PNODE); //对链表进行排序
int main(void)
{
PNODE pHead = NULL; //等价于 struct Node * pHead = NULL;
int val;
pHead = create_list(); /*create_list()功能:创建一个非循环单链表,并
将该链表的头结点的地址付给pHead*/
traverse_list(pHead);
//insert_list(pHead, -4, 33);
if ( delete_list(pHead, 4, &val) )
{
printf("删除成功,您删除的元素是: %d\n", val);
}
else
{
printf("删除失败!您删除的元素不存在!\n");
}
traverse_list(pHead);
//int len = length_list(pHead);
//printf("链表的长度是%d\n", len);
//sort_list(pHead);
//traverse_list(pHead);
/* if ( is_empty(pHead) )
printf("链表为空!\n");
else
printf("链表不空!\n");
*/
return 0;
}
PNODE create_list(void)
{
int len; //用来存放有效节点的个数
int i;
int val; //用来临时存放用户输入的结点的值
//分配了一个不存放有效数据的头结点
PNODE pHead = (PNODE)malloc(sizeof(NODE));
if (NULL == pHead)
{
printf("分配失败, 程序终止!\n");
exit(-1);
}
PNODE pTail = pHead;
pTail->pNext = NULL;
printf("请输入您需要生成的链表节点的个数: len = ");
scanf("%d", &len);
for (i=0; i<len; ++i)
{
printf("请输入第%d个节点的值: ", i+1);
scanf("%d", &val);
PNODE pNew = (PNODE)malloc(sizeof(NODE));
if (NULL == pNew)
{
printf("分配失败, 程序终止!\n");
exit(-1);
}
pNew->data = val;
pTail->pNext = pNew;
pNew->pNext = NULL;
pTail = pNew;
}
return pHead;
}
void traverse_list(PNODE pHead)
{
PNODE p = pHead->pNext;
while (NULL != p)
{
printf("%d ", p->data);
p = p->pNext;
}
printf("\n");
return;
}
bool is_empty(PNODE pHead)
{
if (NULL == pHead->pNext)
return true;
else
return false;
}
int length_list(PNODE pHead)
{
PNODE p = pHead->pNext;
int len = 0;
while (NULL != p)
{
++len;
p = p->pNext;
}
return len;
}
void sort_list(PNODE pHead)
{
int i, j, t;
int len = length_list(pHead);
PNODE p, q;
for (i=0,p=pHead->pNext; i<len-1; ++i,p=p->pNext)
{
for (j=i+1,q=p->pNext; j<len; ++j,q=q->pNext)
{
if (p->data > q->data) //类似于数组中的: a[i] > a[j]
{
t = p->data;//类似于数组中的: t = a[i];
p->data = q->data; //类似于数组中的: a[i] = a[j];
q->data = t; //类似于数组中的: a[j] = t;
}
}
}
return;
}
/*在pHead所指向链表的第pos个节点的前面插入一个新的结点,该节点的值是val,
并且pos的值是从1开始*/
bool insert_list(PNODE pHead, int pos, int val)
{
int i = 0;
PNODE p = pHead;
while (NULL!=p && i<pos-1)
{
p = p->pNext;
++i;
}
if (i>pos-1 || NULL==p)
return false;
/*如果程序能执行到这一行说明p已经指向了第pos-1个结点,但第pos-1个节点是
否存在无所谓*/
//分配新的结点
PNODE pNew = (PNODE)malloc(sizeof(NODE));
if (NULL == pNew)
{
printf("动态分配内存失败!\n");
exit(-1);
}
pNew->data = val;
//将新的结点存入p节点的后面
PNODE q = p->pNext;
p->pNext = pNew;
pNew->pNext = q;
return true;
}
bool delete_list(PNODE pHead, int pos, int * pVal)
{
int i = 0;
PNODE p = pHead;
while (NULL!=p->pNext && i<pos-1)
{
p = p->pNext;
++i;
}
if (i>pos-1 || NULL==p->pNext)
return false;
//如果程序能执行到这一行说明p已经指向了第pos-1个结点,并且第pos个节点是存在的
PNODE q = p->pNext; //q指向待删除的结点
*pVal = q->data;
//删除p节点后面的结点
p->pNext = p->pNext->pNext;
//释放q所指向的节点所占的内存
free(q);
q = NULL;
return true;
}
3.3 线性结构的两种常见应用之一:栈
栈的定义:
一种可以实现“先进后出” 的存储结构;
栈类似于一个箱子,先放进去的东西被压在最下面,先放进去的东西会被最后拿出来。而后放进去的东西会被先拿
出来。
栈的分类
静态栈 (类似于用数组实现,其是一种阉割的数组,即对数组加了“先进后出”的限制)
动态栈 (类似于用链表实现,其是一种严格的链表,即对链表加了“先进后出”的限制)
静态栈必须提前确定栈的大小(有限的),并且都是连续的.
动态栈可以无限大小(内存够的情况下),并且是不连续的.
栈的实现需要确定栈的顶部与栈的底部.
算法:
压栈;出栈。
栈的应用:函数调用;中断;表达式求值;内存分配;缓冲处理;迷宫
栈算法演示图:
想要对栈更深一步的理解,强烈建议去看:https://blog.csdn.net/weixin_42947972/article/details/100642839
下面的示例代码是以动态栈为例,如果你不太懂此例,请去看上面的这个网址,该博客对此示例代码进行了详细的解释。
# include <stdio.h>
# include <malloc.h>
# include <stdlib.h>
typedef struct Node
{
int data;
struct Node * pNext;
}NODE, * PNODE;
typedef struct Stack
{
PNODE pTop;
PNODE pBottom;
}STACK, * PSTACK; //PSTACK 等价于 struct STACK *
void init(PSTACK);//初始化
void push(PSTACK, int );//压栈
void traverse(PSTACK);//遍历
bool pop(PSTACK, int *);//出栈
void clear(PSTACK pS);//清空栈,即使得pS->pTop = pS->pBottom
int main(void)
{
STACK S; //STACK 等价于 struct Stack
int val;
init(&S); //目的是造出一个空栈
push(&S, 1); //压栈
push(&S, 2);
push(&S, 3);
push(&S, 4);
push(&S, 5);
push(&S, 6);
traverse(&S); //遍历输出
clear(&S);
//traverse(&S); //遍历输出
if ( pop(&S, &val) )
{
printf("出栈成功,出栈的元素是%d\n", val);
}
else
{
printf("出栈失败!\n");
}
traverse(&S); //遍历输出
return 0;
}
void init(PSTACK pS)
{
pS->pTop = (PNODE)malloc(sizeof(NODE));
if (NULL == pS->pTop)
{
printf("动态内存分配失败!\n");
exit(-1);
}
else
{
pS->pBottom = pS->pTop;
pS->pTop->pNext = NULL; //pS->Bottom->pNext = NULL;
}
}
void push(PSTACK pS, int val)
{
PNODE pNew = (PNODE)malloc(sizeof(NODE));
pNew->data = val;
pNew->pNext = pS->pTop; //pS->Top不能改成pS->Bottom
pS->pTop = pNew;
return;
}
void traverse(PSTACK pS)
{
PNODE p = pS->pTop;
while (p != pS->pBottom)
{
printf("%d ", p->data);
p = p->pNext;
}
printf("\n");
return;
}
bool empty(PSTACK pS)
{
if (pS->pTop == pS->pBottom)
return true;
else
return false;
}
/*把pS所指向的栈出栈一次,并把出栈的元素存入pVal形参所指向的变量中,如果出
栈失败,返回false,否则返回true*/
bool pop(PSTACK pS, int * pVal)
{
if ( empty(pS) ) //pS本身存放的就是S的地址
{
return false;
}
else
{
PNODE r = pS->pTop;
*pVal = r->data;
pS->pTop = r->pNext;
free(r);
r = NULL;
return true;
}
}
//clear清空
void clear(PSTACK pS)
{
if (empty(pS))
{
return;
}
else
{
PNODE p = pS->pTop;
PNODE q = NULL;
while (p != pS->pBottom)
{
q = p->pNext;
free(p);
p = q;
}
pS->pTop = pS->pBottom;
}
}
3.4线性结构的两种常见应用之二:队列
本节笔记写的较为简单,如果你没看懂,强烈建议你去看这篇文章(这篇文章和本笔记出自同一视频):
https://blog.csdn.net/weixin_42947972/article/details/100678083
当然了,你也可以再去看视频。
队列定义:
一种可以实现“先进先出”的存储结构。
分类:
链式队列:用链表实现
静态队列:用数组实现
静态队列通常都必须是循环队列,为了减少内存浪费。
循环队列的讲解:
1、 静态队列为什么必须是循环队列
2、 循环队列需要几个参数来确定 及其含义
需要2个参数来确定
front
rear
3、 循环队列各个参数的含义
2个参数不同场合不同的含义?
建议初学者先记住,然后慢慢体会
1)队列初始化
front和rear的值都是零
2)队列非空
front代表队列的第一个元素
rear代表了最后一个有效元素 的 下一个元素
3)队列空
front和rear的值相等,但是不一定是零
4、 循环队列入队伪算法讲解
两步完成:
1)将值存入r所代表的位置
2)将r后移,正确写法是 rear = (rear+1)%数组长度
错误写法:rear=rear+1;
5、 循环队列出队伪算法讲解
front = (front+1) % 数组长度
6、 如何判断循环队列是否为空
如果front与rear的值相等,
则队列一定为空
7、 如何判断循环队列是否已满
预备知识:
front的值和rear的值没有规律,
即front可以大于、小于、等于rear
判断循环队列是否已满两种方式:
1、多增加一个表示是否已满的标识参数
2、少用一个队列中的元素(才少用一个,不影响的)
通常使用第二种方式
如果r和f的值紧挨着且r在f的后面,则队列已满
用C语言伪算法表示就是:
if( (r+1)%数组长度 == f )
已满
else
不满
队列算法:
入队
出队
队列的具体应用:
所有和时间有关的操作都有队列的影子。
(例如操作系统认为先进来的先处理)
链式队列(静态队列、循环队列)样例代码
# include <stdio.h>
# include <malloc.h>
# include <stdboo.h>
typedef struct Queue
{
int * pBase;//循环数组的首地址
int front;//队头
int rear;//队尾
}QUEUE;
//初始化队列
void init(QUEUE *);
//判断队列是否为空
bool is_empty(QUEUE *);
//队列是否已满
bool is_full(QUEUE *);
//插入队列 入队
bool en_queue(QUEUE *,int val);
//遍历队列
void traverse_queue(QUEUE *);
//出队
bool out_queue(QUEUE *,int *);
int main(void)
{
int val;//出队的元素
QUEUE Q;
init(&Q);
en_queue(&Q,1);
en_queue(&Q,2);
en_queue(&Q,3);
en_queue(&Q,4);
en_queue(&Q,5);
en_queue(&Q,6);
traverse_queue(&Q);
if(out_queue(&Q,&val))
{
printf("The elem out queue is %d .",val);
}
return 0;
}
void init(QUEUE * pQ)
{
pQ->pBase = (int *)malloc(sizeof(int) * 6);
pQ->front = 0;
pQ->rear = 0;
}
bool is_empty(QUEUE * pQ)
{
return pQ->front == pQ->rear? true:false;
}
bool is_full(QUEUE * pQ)
{
return pQ->front == (pQ->rear + 1) % 6 ? true:false;
}
//入队
bool en_queue(QUEUE * pQ,int val)
{
if(is_full(pQ))
return false;
else
{
pQ->pBase[pQ->rear] = val;
pQ->rear = (pQ->rear + 1) % 6;
return true;
}
}
//遍历队列
void traverse_queue(QUEUE * pQ)
{
if(is_empty(pQ))
{
printf("The queue is empty!");
return;
}
else
{
int val;
while(pQ->front != pQ->rear)
{
printf("%d ",pQ->pBase[pQ->front]);
pQ->front = (pQ->front + 1) % 6;
}
printf("\n");
return;
}
}
//出队
bool out_queue(QUEUE * pQ,int *pVal)
{
if(is_empty(pQ))
{
printf("The queue is empty.");
return false;
}
else
{
*pVal = pQ->pBase[pQ->front];
pQ->front = (pQ->front + 1) % 6;
return true;
}
}
四、专题【递归】
4.1 递归概述
专题:递归【这对你的编码能力是个质的飞跃,如果你想成为一个很厉害的
程序员,数据结构是必须要掌握的,因为计算机专业的本科生也达不到这水
平!计算机特别适合用递归的思想来解决问题,但是我们人类用递归的思想
来考虑问题就会感到十分困扰,这也是很多学过递归的人一直都搞不明白的
地方!那是不是递归可以随便写,当然不是,有些同学一用递归程序就死翘
翘了。递归的思想是软件思想的基本思想之一,在树和图论上面,几乎全是
用递归来实现的,最简单,像求阶乘这种没有明确执行次数的问题,都是用
递归来解决】
定义:
一个函数自己直接或间接调用自己
递归满足的三个条件:
1、递归必须得有一个明确的终止条件
2、该函数处理的数据规模必须在递减
3、这个转化必须是可解的。
循环和递归:
理论上循环能解决的,肯定可以转化为递归,但是这个
过程是复杂的数学转化过程,递归能解决的不一定能转化
为循环,我们初学者只要把经典的递归算法看懂就行,
至于有没有能力运用看个人。
递归:
易于理解
速度慢(为何速度慢,这是因为递归需要反复调用自己,而调用自己和调用)
普通函数是一样的,需要压栈和出栈。反复调用自身函数导致速度慢。
存储空间大
循环
不易于理解
速度快
存储空间小
递归举例:
1.求阶乘
2.1+2+3+4+......+100的和
3.汉诺塔
【汉诺塔】这不是线性递归,这是非线性递归!
n=1 1
n=2 3
n=3 7
.........
.........
n=64 2的64次方减1【这是个天文数字,就算世界上最快的计算机
也解决不了,汉诺塔的负责是2的n次方减1】问题很复杂,但真正解决
问题的编码只有三句。具体看下面的例子。
4.走迷宫(CS的实现)
递归的运用:
树和森林就是以递归的方式定义的;
树和图的很多算法都是以递归来实现的;
很多数学公式就是以递归的方式定义的,如:
斐波拉契序列
1 2 3 5 8 13 21 34......
一个函数为什么可以自己调自己:
4.2 求阶乘
求阶乘的循环实现:
# include <stdio.h>
int main(void)
{
int val;
int i, mult=1;
printf("请输入一个数字: ");
printf("val = ");
scanf("%d", &val);
for (i=1; i<=val; ++i)
mult = mult * i;
printf("%d的阶乘是:%d\n", val, mult);
return 0;
}
求阶乘的递归实现:
# include <stdio.h>
//假定n的值是1或大于1的值
long f(long n)
{
if (1 == n)
return 1;
else
return f(n-1) * n;
}
int main(void)
{
printf("%ld\n", f(100));
return 0;
}
4.3 递归实现1+2+3+4+…+100的和
# include <stdio.h>
long sum(int n)
{
if (1 == n)
return 1;
else
return n + sum(n-1);
}
int main(void)
{
printf("%ld\n", sum(100));
return 0;
}
4.4 汉诺塔
如果你不理解这个伪算法,强烈建议请去看:
https://blog.csdn.net/weixin_42947972/article/details/100679342
# include <stdio.h>
void hanoi(int,char,char,char);
int main(void)
{
//柱子编号
char ch1 = 'A';
char ch2 = 'B';
char ch3 = 'C';
//盘子数量
int n;
printf("请输入盘子的数量");
scanf("%d",&n);
hanoi(n,'A','B','C');//这里的ABC是指三个柱子
return 0;
}
void hanoi(int n,char 'A',char 'B',char 'C')/*这里的ABC不是柱子的含义
了,是指把第二个参数借助于第三个参数挪到第四个参数*/
{
if (1 == n)
printf("编号为%d的盘子:%c-->%c\n",n,'A',"C");
else
{
hanoi(n-1,A,C,B);
printf("编号为%d的盘子:%c-->%c\n",n,'A',"C");
hanoi(n-1,B,A,C);
}
}
五、模块二:非线性结构
强烈建议去看此笔记:
https://blog.csdn.net/weixin_42947972/article/details/100680077
非线性结构
为何数据结构难学:因为计算机内存是线性一维的,而我们要处理的数据
都是比较复杂的,那么怎么把这么多复杂的数据保存在计算机中来保存本
身就是一个难题,而计算机在保存线性结构的时候比较好理解,尤其是数
组和链表只不过是连续和离散的问题,线性结构是我们学习的重点,因为
线性算法比较成熟,无论C++还是Java中都有相关的工具例如Arraylist.
Linkedlist,但是在Java中没有树和图,因为非线性结构太复杂了,他的
操作远远大于线性结构的操作。即使SUN公司也没造出来。
(现在人类还没有造出一个容器,能把树和图都装进去的,因为他们确实是太复杂了)(都要靠链表去实现)
非线性结构:
树
树定义
专业定义:
1、有且只有一个称为根的节点
2、有若干个互不相交的子树,这些子树本身也是一棵树
通俗定义:
1、树是由节点和边组成
2、每个节点只有一个父节点但可以有多个子节点
3、但有一个节点例外,该节点没有根节点,此节点称为根节点
专业术语
节点 父节点 子节点
子孙 堂兄弟
深度:
从根节点到最底层节点的层数称之为深度
根节点是第一层
叶子节点;(叶子就不能劈叉了)
没有子节点的节点
非终端节点:
实际就是非叶子节点。
根节点既可以是叶子也可以是非叶子节点
度:
子节点的个数称为度。(一棵树看最大的)
树分类:
一般树
任意一个节点的子节点的个数都不受限制
二叉树(有序树)
任意一个节点的子节点的个数最多两个,且子节点
的位置不可更改。
二叉树分类:
一般二叉树
满二叉树
在不增加树的层数的前提下。无法再多
添加一个节点的二叉树就是满二叉树。
完全二叉树
如果只是删除了满二叉树最底层最右边的
连续若干个节点,这样形成的二叉树就是
完全二叉树。
森林
n个互不相交的树的集合
一般的二叉树要以数组的方式存储,要先转化成完全二叉树,因为如果你
只存有效节点(无论先序,中序,后序),则无法知道这个树的组成方式
是什么样子的。
树的存储(都是转化成二叉树来存储)
二叉树的存储
连续存储【完全二叉树】
优点:
查找某个节点的父节点和子节点(也包括判断有没有某节点)速度很快
缺点:
耗用内存空间过大
链式存储
一般树的存储
双亲表示法
求父节点方便
孩子表示法
求子节点方便
双亲孩子表示法
求父节点和子节点都很方便
二叉树表示法
把一个普通树转化成二叉树来存储
具体转换方法:
设法保证任意一个节点的
左指针域指向它的第一个孩子
有指针域指向它的下一个兄弟
只要能满足此条件,就可以把一个普通树转化成二叉树
一个普通树转化成的二叉树一定没有右子树
森林的存储
先把森林转化为二叉树,再存储二叉树,具体方式为:根节点
之间可以当成是兄弟来看待
二叉树操作
遍历
先序遍历【先访问根节点】
先访问根节点
再先序访问左子树
再先序访问右子树
中序遍历【中间访问根节点】
中序遍历左子树
再访问根节点
再中序遍历右子树
后序遍历【最后访问根节点】
先后序遍历左子树
再后序遍历右子树
再访问根节点
已知两种遍历序列求原始二叉树:
通过先序和中序 或者 中序和后续我们可以
还原出原始的二叉树
但是通过先序和后续是无法还原出原始的二叉树的
换种说法:
只有通过先序和中序, 或通过中序和后序
我们才可以唯一的确定一个二叉树
树的应用
树是数据库中数据组织的一种重要形式(例如图书馆
的图书分类一层一层往下分。)
操作系统子父进程的关系本身就是一棵树
面向对象语言中类的继承关系本身就是一棵树
赫夫曼树(树的一个特例)
图(本笔记没讲)
静态链式二叉树的代码实现举例
实现下图的二叉树:
# include <stdio.h>
# include <stdlib.h>
typedef struct BinaryTree
{
char data; //数据域
struct BinaryTree *pLeft; //左子树
struct BinaryTree *pRight; //右子树
} BTNODE,* PBTNODE;
PBTNODE ApplyNode(void); //申请节点
void PreTraverseBTree(PBTNODE pT); //遍历输出先序
void InTraverseBTree(PBTNODE pT); //遍历输出中序
void PostTraverseBTree(PBTNODE pT); //遍历输出后序
int main(void)
{
PBTNODE pT;
pT = ApplyNode(); //申请节点
PreTraverseBTree(pT); //先序遍历输出
printf("\n");
InTraverseBTree(pT); //中序遍历输出
printf("\n");
PostTraverseBTree(pT); //后序遍历输出
printf("\n");
return 0;
}
/* 申请节点 */
PBTNODE ApplyNode(void)
{
PBTNODE pA = (PBTNODE)malloc(sizeof(BTNODE));
PBTNODE pB = (PBTNODE)malloc(sizeof(BTNODE));
PBTNODE pC = (PBTNODE)malloc(sizeof(BTNODE));
PBTNODE pD = (PBTNODE)malloc(sizeof(BTNODE));
PBTNODE pE = (PBTNODE)malloc(sizeof(BTNODE));
PBTNODE pF = (PBTNODE)malloc(sizeof(BTNODE));
pA->data = 'A';
pB->data = 'B';
pC->data = 'C';
pD->data = 'D';
pE->data = 'E';
pF->data = 'F';
pA->pLeft = pB;
pA->pRight = pC;
pB->pLeft = pD;
pB->pRight = pE;
pC->pLeft = NULL;
pC->pRight = pF;
pD->pLeft = pD->pRight = NULL;
pE->pLeft = pE->pRight = NULL;
pF->pLeft = pF->pRight = NULL;
return pA;// 返回头节点
}
/* 递归遍历输出先序 */
void PreTraverseBTree(PBTNODE pT)
{
if(pT != NULL)
{
printf("%c",pT->data);//先输出根节点
if(pT->pLeft != NULL)
{
PreTraverseBTree(pT->pLeft); //再遍历左子树
}
if(pT->pRight != NULL )
{
PreTraverseBTree(pT->pRight); //最后遍历右子树
}
}
}
/*递归遍历输出中序*/
void InTraverseBTree(PBTNODE pT)
{
if(pT != NULL)
{
if(pT->pLeft != NULL)
{
PreTraverseBTree(pT->pLeft); //遍历左子树
}
printf("%c",pT->data);
if(pT->pRight != NULL )
{
PreTraverseBTree(pT->pRight); //遍历右子树
}
}
}
/* 遍历输出后序 */
void PostTraverseBTree(PBTNODE pT)
{
if(pT != NULL)
{
if(pT->pLeft != NULL)
{
PreTraverseBTree(pT->pLeft); //遍历左子树
}
if(pT->pRight != NULL )
{
PreTraverseBTree(pT->pRight); //遍历右子树
}
printf("%c",pT->data);
}
}
六、查找和排序
6.1 概述
查找:
折半查找
排序:
冒泡
插入
选择
快速排序
归并排序
排序和查找的关系:
排序是查找的前提
排序是重点
(本笔记没有查找相关的内容)
6.2冒泡排序
//简单冒泡排序举例【升序】
# include <stdio.h>
void bubble_sort(int *,int);
int main(void)
{
int i;
int len = 6;
int arr[len] = {
2,10,8,5,3,1};
bubble_sort(arr,len);
for(i = 0;i < 6;i++)
printf("%d ",arr[i]);
printf("\n");
return 0;
}
void bubble_sort(int * arr,int len)
{
int i,j,t;
for(i = 0; i < len; i++)
{
for(j = i+1; j < len-1; j++)
{
if(arr[i] > arr[j])
{
t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}
}
}
6.3插入排序
//直接插入排序【升序】
# include <stdio.h>
void insertion_sort(int *,int);
int main(void)
{
int i;
int len = 6;//数组长度
int arr[len] = {
2,10,8,5,3,1};
insertion_sort(arr,len);
for(i = 0;i < 6;i++)
printf("%d ",arr[i]);
printf("\n");
}
void insertion_sort(int * arr,int len)
{
int i,j,t;
for (i=1; i<len; i++)
{
if(arr[i]<arr[i-1])
{
t = arr[i];
j = i - 1;
for(j; j>=0 && arr[j]>t;j--)
{
arr[j+1] = arr[j];
}
a[j+1] = t;
}
}
}
6.4选择排序
//直接选择排序【升序】
# include <stdio.h>
void selection_sort(int *,int);
int main(void)
{
int i;
int len = 6;//数组长度
int arr[6] = {
2,10,8,5,3,1};
selection_sort(arr,len);
for(i = 0;i < 6;i++)
printf("%d ",arr[i]);
printf("\n");
return 0;
}
void selection_sort(int * arr,int len)
{
int i,j,t,min;
for(i=0; i<len-1; i++)
{
for(min=i,j=i+1; j<len; j++)
{
if(arr[min] < arr[j])
min = j;
}
if(min != j)
{
t = arr[i];
arr[i] = arr[min];
arr[min] = t;
}
}
}
6.5快速排序
# include <stdio.h>
void quick_sort(int *,int,int);
int find_pos(int *,int,int);
int main(void)
{
int i,len,low,high;
low = 0;
high = 6;
int arr[7] = {
13,2,1,3,8,5,1};
quick_sort(arr,low,high);//low表示起始位置,high表示结束位置
for(i = 0;i < 7;i++)
printf("%d ",arr[i]);
printf("\n");
return 0;
}
void quick_sort(int * arr,int low,int high)
{
int pos;
if (low < high)
{
pos = find_pos(arr,low,high);
quick_sort(arr,low,pos-1);
quick_sort(arr,pos+1,high);
}
}
int find_pos(int * arr,int low,int high)
{
int val = arr[low];
while(low < high)
{
while(low < high && arr[high] >= val)
--high;
arr[low] = arr[high];
while(low < high && arr[low] <= val)
++low;
arr[high] = arr[low];
}
arr[low] = val;
return high;
}
6.6归并排序
# include <stdio.h>
void merge_sort(int arr[], const int len);
void merge_sort_recursive(int arr[], int reg[], int start, int end);
int main(void)
{
int i;
int arr[7] = {
13,2,1,3,8,5,1};
merge_sort(arr,7);
for(i = 0;i < 7;i++)
printf("%d ",arr[i]);
printf("\n");
return 0;
}
void merge_sort_recursive(int arr[], int reg[], int start, int end) {
if (start >= end)
return;
int len = end - start, mid = (len >> 1) + start;
int start1 = start, end1 = mid;
int start2 = mid + 1, end2 = end;
merge_sort_recursive(arr, reg, start1, end1);
merge_sort_recursive(arr, reg, start2, end2);
int k = start;
while (start1 <= end1 && start2 <= end2)
reg[k++] = arr[start1] < arr[start2] ? arr[start1++] : arr[start2++];
while (start1 <= end1)
reg[k++] = arr[start1++];
while (start2 <= end2)
reg[k++] = arr[start2++];
for (k = start; k <= end; k++)
arr[k] = reg[k];
}
void merge_sort(int arr[], const int len) {
int reg[len];
merge_sort_recursive(arr, reg, 0, len - 1);
}