数据结构概述
定义:我们如何把现实中大量而复杂的问题以特定的数据类型和特定的存储结构保存到主存储器(内存)中,以及在此基础上为实现某个功能(比如查找某个元素,删除某个元素、对所有元素进行排序)而执行的相应操作,这个相应的操作也叫算法。
数据结构=个体+个体的关系
算法=对存储数据的操作(依附于存储的,存储方式不一样,算法也不一样)
算法:
解题的方法和步骤
衡量算法的标准:1.时间复杂度 (大概程序要执行的次数,而非执行的时间)2.空间复杂度(算法执行过程中大概所占用的最大内存)3.难易程度 4.健壮性
数据结构的地位:数据结构是软件中最核心的课程
程序=数据的存储+数据的操作+可以被计算机执行的语言
预备知识:
指针:指针是C语言的灵魂,
CPU访问内存的3条线:地址线、控制线、数据线
定义:地址,内存单元的编号,从0开始的非负整数,范围0-FFFFFFFF(4G-1)
指针:指针就是地址,地址就是指针
指针变量是存放内存单元地址的变量
指针的本质是一个操作受限的非负整数
*p是不确定的内存存储地址,垃圾数据,不允许赋值
一个软件所分配到的空间中极可能存在着以前其他软件使用过后的残留数据,这些数据被称之为垃圾数据。所以通常情况下我们为一个变量,为一个数组,分配好存储空间之后都要对该内存空间初始化。
注意:
指针变量也是变量,只不过它存放的不能是内存单元的内容,只能存放内存单元的地址
普通变量前不能加*
常量和表达式前不能加&
指针变量也是变量,只不过它存放的不能是内存单元的内存
基本概念
int i=10;
int *P=&i; //等价于 int*p; P=&i:
详解这两步操作:
1.p存放了i的地址,所以我们说p指向了i
2.p和i是完全不同的两个变量,修改其中的任意一个变量的值不影响另一个;
3.p指向i,*p就是i变量本身,更形象地说所有出现*p的地方都可以换乘i,i的地方都可以换成*p
如何通过被调函数修改主调函数中普通变量的值:
1.实参为相关变量的地址;2.形参为以该变量的类型为类型的指针变量;3.在被调函数中通过“*形参变量名”的方式就可以修改主函数
int *p //p是个指针变量,int*表示该p变量只能存储int类型变量的地址。
指针和数组:
指针和一维数组
数组名:1.一维数组名是个指针常量;2.它存放的是一维数组第一个元素的地址;3.它的值不能被改变;4.一维数组名指向的是数组的第一个元素
下标和指针的关系:a[i]等价于*(a+i)
假设指针变量的名字为p,则p+i的值是p+i*(p所指向的变量所占的字节数)
指针变量的运算: 1.指针变量不能相加,不能相乘,不能相除;2.如果两指针变量属于同一数组,则可以相减;3.指针变量可以加减一整数,前提是最终结果不能超过指针 a [3]==*(3+a)
举例:
如何通过被调函数修改主调函数中一维数组的内容:
两个参数:1.存放数组首元素的指针变量;2.存放数组元素长度的整型变量;
一个字节8位,1个字节一个地址
所有的指针变量只占4个字节,用第一个字节的地址表示整个变量的地址
第7课:如何通过函数修改实参的值?
要想改写值,只需改写指针的变量
结构体
1.为什么会出现结构体:为了表示一些复杂的数据,而普通的基本类型变量无法满足要求。
struct Studnet
{
int sid;
String name;
int sage;
}
2.什么叫结构体:结构体是用户根据实际需要自己定义的符合数据类型;
如何使用结构体:
两种方式:
struct Student st={1000,"zhangsan",200};
struct Strudeng *pst=&st;
1.st.sid
2.pst->sid
pst所指向的结构体变量中sid这个成员
注意事项:
1.结构体变量不能加减乘除,但可以相互赋值;
2.普通结构体变量和结构体指针变量作为喊出传参的问题
动态内存的分配与释放
模块一:线性结构【把所有的节点用一根直线穿起来】
连续存储【数组】
1.什么叫数组:
元素类型相同,大小相同
2.数组的优缺点:
离散存储【链表】
typedef的用法:为已有的数据类型起个名字,例如:typedef int ZHANGSAN;//为int再重新多取一个名字,ZHANGSAN等价于int
定义:1.N个节点离散分配;2.彼此通过指针相连;3.每个节点只有一个前驱节点,每个节点只有一个后续节点;4.首节点没有前驱节点,尾节点没有后续节点;
专业术语:
首节点:第一个有效节点
尾节点:最后一个有效节点
头结点:头结点的数据类型和首节点类型一样,第一个有效节点之前的那个节点;头结点并不存放有效数据;加头结点的目的主要是为了方便对链表的操作;
头指针:指向头结点的指针变量
尾指针:指向尾节点的指针变量
如果希望通过一个函数来对链表进行处理,我们至少需要接受链表的哪些参数:只需要一个参数:头指针,因为我们通过头指针可以推算出链表的其他参数。
分类:1.单链表:2.双链表:每一个节点有两个指针;3.循环链表:能通过任何一个节点,找到其他所有的节点;4.非循环链表:
算法:
1.遍历 2.查找 3.清空 4.销毁 5.求长度 6.排序 7.删除节点:8.插入节点:
非循环单链表插入节点伪算法:
r->p->pNext; p->pNext=q; q->pNext=r;
删除非循环单链表节点伪算法:
r=p->pNext;
p->pNext=p->pNext->pNext;(单独这一条容易内存泄漏)
free(r);
注意:free p;//删除p指向节点所占的内存,不是删除p本身所占内存;
p->pNext; //p所指向结构体变量中的pNext成员本身;
算法:
狭义的算法是与数据的存储方式密切相关;广义的算法是与数据的存储方式无关
泛型:利用某种技术达到的效果就是不同的存储方式,执行的操作是一样的
链表的优缺点:
线性结构的两种常见应用之一 栈
1.定义:一种可以实现“先进后出”的存储结构,栈类似于箱子
2.分类:
静态栈:这种分配策略是将整个数据的数据空间设计为一个栈.每当调用一个过程时,它所需要的数据空间就分配在栈顶,每当工作结束时就释放这部分空间.过程所需的数据空间包括两部分:一部分是生存期在本过程这次活动中的数据对象,如局部变量,参数单元,临时变量等等;另一部分则是用以管理过程活动的记录信息.即当一次过程调用出现时,调用该过程的那个过程的活动即被中断,当前机器的状态信息,诸如程序计数器(返回地址).,存器的值等等,也都必须保留在栈中.当控制从调用返回时,便根据栈中记录的信息恢复机器状态,使该过程的活动继续进行
动态栈:如果一个程序语言提供用户自由地申请数据空间和退还数据空间的机制(如new init),或者不仅有过程而且有进程的程序结构的情况下,空间的使用未必服"先申请后释放,后申请先释放"的原则,那么栈式的动态分配方案就不适用了.通常使用一种称为堆式的动态存储分配方案.假设程序允许时有一个大的存储空间,每当需要时就从这片空间中借用一块,不用时再退还,由于借还的时间先后不一,经一段运行之后,程序运行空间将被分划成许多块块,有些占用,有些空闲.那么当运行程序要求一块体积为N的空间时,需要决定应该从哪个空闲块得到这个空间.理论上讲,应该从比N稍大一些的空闲块中取出N个单元,以便使大的空闲块派更大的用场,但实际难度很大,实际中常常采用的办法是:先遇到哪块比N大就从其中取出N个单元.即使这样,也会发生找不到一块比N大的空闲块,但所有空闲块的总和比N大得多的情况,这时,有的分配管理系统采用废品回收的办法来应付.
3.算法:
出栈
压栈
4.应用:top\ bottom
a .函数调用;b.中断;c.表达式求值;d.内存分配
线性结构的两种常见的应用之二 队列
定义:一种可以实现“先进先出”的存储结构(出队、入队)
分类:
1.链式队列--用链表实现:
2.静态队列-用数组实现:静态队列通常都必须是循环队列
循环队列的讲解:
1.静态队列为什么必须是循环队列:
静态队列通常都必须是循环队列,为了减少内存浪费;如果用传统意义的数组实现队列,无论入队还是出队,rear和front指针只能+不能-;比F元素下标小的数组元素下标就浪费了。
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.如何判断队列是否已满
上图这种情况,如果再插入f,r指向同一个元素。如果这样的话就不能判断队列是空还是满。
所以为了判断循环队列是否已满,有一下两种方式:
1、多增加一个表标识的参数
2、少用一个队列中的元素(才一个,不影响的)
如果r和f紧挨着(r的下一个位置是f),则队列已满
队列的具体应用:所有和时间有关的操作都有队列的影子
专题:递归
定义:一个函数直接调用自己或者通过一系列的调用语句间接地调用自己的函数,称做递归函数;
当在一个函数的运行期间调用另一额函数时,在运行被调用函数之前,系统需先完成3件事:(1)将所有的实参、返回地址等信息传递给被调用函数保存;(2)为被调用函数的局部变量分配存储区;(3)将控制转移到被调函数的入口,而从被调用函数返回调用函数之前,系统也应完成3件工作:(1)保存被调函数的计算结果;(2)释放被调函数的数据区;(3)依照被调函数保存的返回地址将控制转移到调用函数。当有多个函数构成嵌套调用时,按照“后调用先返回”的原则,上述函数之前的信息传递和控制转移必须通过“栈”来实现,即系统将整个程序运行时所需的数据空间安排在一个栈中,每当调用一个函数时,就为它在栈顶分配一个存储区,每当从一个函数退出时,就释放它的存储区,则当前正运行的函数的数据区必在栈顶。
举例:
1.1+2+3+4+...100的和
2.求阶乘
3.汉诺塔
汉诺塔(又称河内塔)问题是源于印度一个古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。有预言说,这件事完成时宇宙会在一瞬间闪电式毁灭。也有人相信婆罗门至今还在一刻不停地搬动着圆盘
递归满足三个条件:
1.递归必须得有一个明确的终止条件;2、该函数所处理的数据规模必须在递减;3.这个转化必须是可解的。
循环和递归
1.递归:易于理解、速度慢、存储空间大
2.循环:不易理解、速度快、存储空间小
对计算机而言,一个函数调用别的函数与调用它自己是没有区别的。对机器而言,递归和普通函数的调用没有区别,都是借用堆栈把函数的返回地址,函数的局部变量,调用函数的实参压栈。
递归的思想是软件思想的最基本思想之一,在树和图论上面,几乎全是用递归来实现的,最简单的,大家用的资源管理器,我邀请无纸巾的创建用户目录,这实际就是冬天构造树的问题,要用到递归的知识来解决。
递归的应用:
树和森林就是以递归的方式定义的
数和图的很多算法
很多数学公式就是以递归的方式定义的。
菲波拉契序列:1 2 3 5 8 13 21 34。
复习内容:
逻辑结构: 线性(数组、链表)、
注意:栈和队列是一种特殊的线性结构
非线性(树、图)
物理结构:
4.走迷宫
模块二:非线性结构
树
专业定义:
1.有且只有一个称为根的节点
2.有若干个互不相交的子树,这些子树也是一棵树
通俗定义:
1.树是有节点和边组成
2.每个节点只有一个父节点,但可以有多个子节点
3.但有一个节点例外,该节点没有父节点,此节点称为根节点
专业术语
节点 父节点 子节点 子孙 堂兄弟、双亲
1.度(Degree):结点拥有的子树数称为结点的度。A的度为3,C的度为1,F的度为0,
2.叶子(Leaf):度为0的结点称为叶子(leaf)或者结点.结点k、L、F、G、M、I、J都是树的叶子。(没有子结点的结点)
3.度不为0的结点称为非终端结点或分支结点。除了根结点之外,分支结点也称为内部结点。(实际就是非叶子节点)
4.树的度是树内各结点的度的最大值。如B树的度为3,。结点的子树的根称为该节点的孩子,相应地,该结点称为孩子的双亲。同一个双亲的孩子之间互称兄弟
5.结点的祖先是从根到该结点所经分支上的所有结点。反之,以某结点为根的子树种的任一结点都称为该结点的子孙
6.深度:树中结点的最大层次称为树的深度(Depth)或高度,B树的深度为4。从根节点到最底层节点的层数称之为深度,根节点是第一层
7.如果将树中结点的各子树看成从左至右是有次序的(即不能互换),则称该树为有序树,否则称为无序树
分类:
一般树:任意一个节点的子节点的个数都不受限制
二叉树:任意一个节点的子节点的个数最多两个,且子节点的位置不可更改
分类:一般二叉树、满二叉树:在不增加树的层数的前提下,无法再多添加一个节点的二叉树就是满二叉树;
完全二叉树:如果只是删除了满二叉树最底层最右边的连续若干个节点,这样形成的二叉树就是完全二叉树
森林:n个互不相交的树的集合
树的存储
二叉树的存储
1.连续存储【完全二叉树】
优点:查找某个节点的子节点或父节点,(也包括判断有没有子节点)速度很快
缺点:耗用内存空间过大
2.链式存储:
一般树的存储
1.双亲表示法:求父节点方便
2.孩子表示法:求子节点方便
3.双亲孩子表示法:求父节点和子节点都很方便
4.二叉树表示法:
把一个普通树转化成二叉树来存储
具体转化方法:1.设法保证任意一个节点的
左指针域指向它的第一个孩子,
右指针指向它下一个兄弟结点
只要能满足此条件,就可以把一个普通树转化为二叉树
一个普通树转化成的二叉树一定没有右子树
森林的存储
操作(重点):
1.二叉树操作
遍历
先序遍历【先访问根节点】:先访问根节点,再先序访问左子树,再先序访问右子树
中序遍历【中间访问根节点】:中序遍历左子树,再访问根节点,再中序遍历右子树
后序遍历【最后访问根节点】:先中序遍历左子树,再中序遍历右子树,再访问根节点
已知两种遍历序列求原始二叉树:
1.通过先序和中序,或者中序和后序我们可以
还原出原始的二叉树
但是通过先序和后序是无法还原出原始的二叉树的
2.换种说法:只有通过先序和中序,或者通过中序和后序
我们才可以唯一的确定一个二叉树
树应用:
1.树是数据库中数据组织一种重要形式
2.操作系统子父进程的关系本身就是一棵树
3.面向对象语言中类的继承关系,本身就是一棵树
4.赫夫曼树
图
模块三:查找和排序
排序是查找的前提
排序是重点
快速排序
折半查找、排序:冒泡、插入、选择、快速排序、归并排序
Java中容器和数据结构相关知识:
Iterator接口、Map 、哈希表
再次讨论什么是数据结构
1.数据结构研究是数据的存储和数据的操作的一门学问
2.数据的存储分为两部分:个体的存储、个体关系的存储
从某个角度而言,数据的存储最核心的就是个体关系的存储,个体的存储可以忽略不计
再次讨论什么是泛型
同一种逻辑结构,无论该逻辑结构物理存储是什么样子的,我们可以对它执行相同的操作。