1.数据结构概述
算法
- 解题的方法和步骤
- 衡量算法的标准
- 时间复杂度:大概程序要执行的次数,而非执行的时间
- 空间复杂度:算法执行过程中大概所占用的最大内存
- 难易程度
- 健壮性
数据结构的地位
数据结构是软件中最核心的课程
程序 = 数据的存储 + 数据的操作 + 可以被计算机执行的语言
2.预备知识
指针
指针是c语言的灵魂
无论指针变量指向的变量占多少个字节,指针统一只占8个字节(64位电脑)
定义
- 地址:内存单元的编号,从0开始的非负整数
- 范围:0-FFFFFFFF[4G-1](32位电脑)
- 指针:指针和指针变量
指针就是地址,地址就是指针
指针变量:是存放内存单元地址的变量
指针的本质是一个操作受限的非负整数
分类
基本类型的指针
- 基本概念
int i = 10;
int * p = &i;
详解这两步操作
1) p存放了i的地址,所以我们说p指向了i
2) p和i是完全不同的两个变量,修改其中的任意一个变量的值不影响另一个变量
3) p指向i,p就是i变量本身,更形象的说所有出现p的地方可以换成i,所有出现i的地方可以换成*p
总结:
-
如果一个指针变量(假定为p)存放了某个普通变量的地址,那我们就可以说p指向i,但p与i是两个不同的变量
-
p等价与i,或者说p可以与i在任何地方互换
-
如果一个指针变量指向了某个普通变量,则*指针变量就完全等价于普通变量。
-
注意
指针变量也是变量,只不过它存放的不能是内存单元的内容,只能存放内存单元的地址,普通变量前不能加*
常量和表达式前不能加& -
如何通过修改被调函数修改主调函数中普通变量的值
-
实参为相关变量的地址
-
形参为该变量的类型为类型的指针变量
-
在被调函数中通过*形参变量名的方式可以修改主函数中普通变量的值
指针和一维数组
- 数组名
一维数组名是个指针常量,它存放的是一维数组第一个元素的地址,它的值不能被改变,一维数组名指向的是数组的第一个元素 - 下标和指针的关系
a[i] 等价于 (a+i)
假设指针变量的名字为p
则p+i的值是p+i(p所指向的变量所占的字节数) - 指针变量的运算
指针变量不能相加,不能相乘,不能相除
如果两个指针变量属于同一数组,则可以相减
指针变量可以减一整数,前提是最终结果不能超过指针
P+i的值是p+i*(p所指向的变量所占的字节数)
p-i的值是p-i*(p所指向的变量所占的字节数)
p++等价于p+1
p—等价于p-1 - 举例-如何通过被调函数修改主调函数中一维数组的内容
两个参数
存放数组首元素的指针变量
存放数组元素长度的整型变量
动态内存的分配和释放
动态构造一维数组
假设动态构造一个int型数组
Int * p = (int *)malloc(int len)
- malloc只有一个int型的形参,表示要求系统分配的字节数
- malloc函数的功能是请求系统分配len个字节的空间,如果请求成功,则返回第一个字节的地址,如果分配失败,则返回NULL
- malloc函数能且只能返回第一个字节的地址,所以我们需要把无实际意义的第一个字节的地址(俗称干地址)转化为一个有实际意义的地址,malloc前面必须加(数据类型*),表示把这个无实际意义的地址强制转化为相应类型的地址
如:
Int * p = (int *)malloc(50) - 判断下面这个程序中的指针p是否指向合法的整型变量
int main()
{ int * p;
fun(&p);
}
Int fun(int **q)
{int s;
*q=&s;
}
不能,在fun函数运行时p指向了s,但当fun运行结束,s的内存单元释放,此时p无法指向合法的整型单元
结构体
为了表示一些复杂的数据结构,而普通的基本类型变量无法满足要求,结构体是用户根据实际需求,自己定义的复合数据类型
两种方式:
struct Student st = {1000,“张三”, 34};
struct Student * pst = &st;
- st.sid
- pst->sid
pst->sid表示pst所指向的结构体变量中的sid这个成员
注意事项:
结构体变量不能加减乘除,但可以相互赋值
普通结构体变量和结构体指针变量作为函数传参的问题
3.模块一:线性结构
线性结构:【把所有的结点用一根线穿起来】
连续存储【数组】
什么叫数组
元素类型相同,大小相等
数组的优缺点
- 优点:存取速度很快
- 缺点:插入删除元素很慢,空间通常有限制;
事先必须知道数组的长度,需要大块连续的内存
创建与数组相关的函数
初始化
增加元素
插入元素
删除元素
判断数组是否为空
判断数组是否为满
排序
打印数组元素
倒置数组
离散存储【链表】
定义
N个节点离散分配
彼此通过指针相连
每个节点只有一个前驱节点,每一个节点只有一个后续节点
首节点没有前驱节点,尾节点没有后续节点
专业术语
-
首节点
第一个有效节点 -
尾节点
最有一个有效节点 -
头节点
头节点的数据类型和首节点类型一样
第一个有效节点之前的那个节点
头节点并不存放有效数据 -
头指针
指向头节点的指针变量 -
尾指针
指向尾节点的指针变量 -
如果希望通过一个函数对链表进行处理,我们至少需要接收链表哪些参数
只需要一个参数:头指针
因为我们通过头指针可以推算出链表的其他所有信息
分类
- 单链表
- 双链表
每一个节点有两个指针域 - 循环链表
能通过任何一个节点找到其他所有的节点 - 非循环链表
算法
遍历
查找
清空
销毁
求长度
排序
删除节点
插入节点
- 狭义的算法是与数据的存储方式密切相关
- 广义的算法与数据的存储方式无关
- 泛型:利用某种技术达到的效果就是:不同的存储方式执行的操作是一样的
链表的优缺点
- 连续存储【数组】
优点:存取速度很快
缺点:插入删除元素很慢,空间通常有限制
事先必须知道数组的长度,需要大块连续的内存 - 离散存储【链表】
优点:空间没有限制
缺点:存取速度很慢
线性结构的两种常见应用之一:栈
定义
静态内存在栈里分配
动态内存在堆里分配
栈:一种可以实现“先进后出”的存储结构
栈类似于箱子,弹夹
栈的本质就是操作受限的链表
分类
静态栈
动态栈
算法
定义栈
pTop:指向栈顶元素
pBottom:指向栈底(非有效节点)
初始化栈
压栈
遍历栈
出栈
清空栈
应用
函数调用
中断
表达式求值
内存分配
缓冲处理
迷宫
线性结构的两种常见应用之二:队列
定义
一种可以实现“先进先出”的存储结构
分类
-
链式队列:用链表实现
-
静态队列:用数组实现
静态队列通常必须是循环队列实现
循环队列的讲解: -
静态队列为什么必须是循环队列
传统数组实现会造成空间浪费
循环队列类似于卷积 -
循环队列需要几个参数确定及其含义
需要2个参数front和rear,2个参数在不同场合有不同的意义(建议初学者先记住,之后慢慢体会)
1) 队列初始化
front和rear的值都是零
2) 队列非空
front代表的是队列的第一个元素
rear代表的是队列的最后一个有效元素的下一个元素
3) 队列空
front和rear的值相等,但不一定是零 -
循环队列入队伪算法讲解
两步完成:
将值存入r所代表的位置
正确的写法:r =( r+1)%数组的长度(r = r+1 ;这样写肯定是错误的) -
循环队列出队伪算法讲解
f=(f+1)%数组的长度 -
如何判断循环队列是否为空
如果front和rear的值相等,该队列就一定为空 -
如何判断循环队列是否已满
预备知识
Front的值可能比rear大,也完全有可能比rear小,当然也可能相等
1)多增加一个标识参数
2)少用一个元素[通常使用第二种方式]
如果r和f的值紧挨着,则队列已满
If ((r+1)%s数组长度 == f)
已满
Else
不满
队列算法
入队
出队
队列的具体应用
所有与时间有关的操作都有队列的影子
4.专题:递归
定义
一个函数自己直接或间接调用自己
举例
- 1+2+3+4…+100
- 求阶乘
- 汉诺塔
伪算法:
If (n>1)
{ - A借助C将前n-1个盘子移动到B
- A将第n个盘子移动到C
- B借助A将前n-1个盘子移动到C
} - 走迷宫
将地图变成非常小的格子
递归满足的三个条件
- 递归必须得有一个明确的终止条件
- 该函数所处理的数据规模必须递减
- 这个转化是可解的
递归三部曲
- 确定递归函数的参数和返回值
- 确定终止条件
- 确定单层递归的逻辑
循环和递归
- 递归:
易于理解
速度慢
存储空间大
- 循环:
不易理解
速度快
存储空间小
递归的应用
树和森林就是以递归的方式定义的
数和图的很多算法都是以递归来实现的
很多数学公式就是以递归的方式定义的
5.模块二:非线性结构
树
树定义
专业定义:
- 有且只有一个成为根的节点
- 有若干个互不相交的子树,这些子树本身也是一棵树
通俗定义: - 树是由节点和边组成
- 每一个节点只有一个父节点但可以有多个子节点
- 但有一个节点例外,该节点没有父节点,此节点称为根节点
树分类
- 一般树:任意一个节点的子节点的个数不受限制
- 二叉树:任意一个节点的子节点的个数最多两个,且子节点的位置不可更改
分类:
一般二叉树:
满二叉树:在不增加树的层数的情况下,不能再增加节点的树
完全二叉树:如果只是删除满二叉树最底层最右边的连续若干个节点,这样形成的二叉树就是完全二叉树。满二叉树是完全二叉树的一个特例。 - 森林:n个互不相交的树的集合
树的存储
二叉树的存储
连续存储【完全二叉树】
优点:查找某个节点的父节点和子节点速度很快(也包括判断有没有子节点)
缺点:耗用的内存空间过大
链式存储
一般树的存储
- 双亲表示法:求父节点方便
- 孩子表示法:求子节点方便
- 双亲孩子表示法:求父节点个子节点都方便
- 二叉树表示法:把一个普通数转化成二叉树来存储
具体转换方法:
设法保证任意一个节点的
左指针域指向它的第一个孩子
右指针域指向它的下一个兄弟
只要能满足此条件,就可以把一个普通树转换成二叉树
一个普通树转化成的二叉树一定没有右子树(左孩子,右兄弟)
森林的存储
先把森林转化成二叉树,再存储二叉树
树操作
树的遍历
- 先序遍历【先访问根节点】
先访问根节点
再先序访问左子树
再先序访问右子树
- 中序遍历【中间访问根节点】
中序遍历左子树
再访问根节点
在中序遍历右子树
- 后序遍历【最后访问根节点】
中序遍历左子树
中序遍历右子树
再访问根节点
已知两种遍历序列求原始二叉树
通过先序和中序或者中序和后序我们可以还原出原始的二叉树
但是通过先序和后序是无法还原出原始的二叉树
换种说法:
只有通过先序和中序或通过中序或后序
我们才可以唯一的确定一个二叉树
-
已知先序和中序还原原始的二叉树
总结:根据先序确定根节点,根据中序确定左子树和右子树 -
已知中序和后序还原原始的二叉树
总结:根据后序确定根节点,根据中序确定左子树和右子树
树的应用
树是数据库中数据组织一种重要形式
操作系统子父进程的关系本身就是一棵树
面向对象语言中类的继承关系
赫夫曼树
图
6.模块三:查找和排序
- 折半查找
- 排序
冒泡
插入
选择
快速排序
归并排序
排序和查找的关系
排序是查找的前提
排序是重点
7.数据结构和泛型
- 数据结构
数据结构是研究数据的存储和数据的操作的一门学问
数据的存储分为两部分:
个体的存储
个体关系的存储
从某个角度而言,数据的存储最核心的就是个体关系的存储
个体的存储可以忽略不计
- 泛型
同一种逻辑结构,无论该逻辑结构的物理存储是什么样子的
我们都可以对它执行相同的操作。