数据结构入门学习笔记
快找工作了,现在开始好好学一下数据结构的知识,看的资料是在b站上找的郝斌老师的数据结构入门的视频,同时自己做个笔记记录一下。这个视频是基于C语言的,我本身是想学习java版的数据结构,思想都是一样的,数据结构里有指针的概念,但java语言中没有指针,所以还是学习了C语言版的数据结构。(学完了数据结构要继续去看郝斌老师的Java了)
数据结构概述
- 定义:我们如何把现实中大量而复杂的问题以特定的数据类型和特定的存储结构保存到主存储器(内存)中,以及在此基础上为实现某个功能(比如查找某个元素,删除某个元素,对所有元素进行排序)而执行的相应操作,这个相应的操作也叫算法。(数据结构是专门研究数据存储的问题)
数据结构 = 个体的存储 + 个体的关系存储
算法 = 对存储数据的操作
数据结构
-
狭义:数据结构是专门研究数据存储的问题
数据的存储包含两个方面:个体的存储 + 个体关系的存储 -
广义:数据结构既包含数据的存储也包含数据的操作
对存储数据的操作就是算法
算法:解题的方法和步骤
衡量算法的标准:
- 时间复杂度:大概程序要执行的次数,而非执行的时间。(因为同一程序在不同机器上执行的时间是不一样的,有差异)
- 空间复杂度:算法执行过程中大概所占用的最大内存。
- 难易程度
- 健壮性
算法:
狭义的算法是与数据的存储方式密切相关
广义的算法是与数据的存储方式无关(这就是泛型思想)
泛型:利用某种技术达到的效果就是:不同的存储方式,执行的操作是一样的
数据结构的地位:数据结构是软件中最核心的课程。
程序 = 数据的存储 + 数据的操作 + 可以被计算机执行的语言
预备知识
指针
指针的重要性:指针是C语言的灵魂。(内存是可以被CPU直接访问的,硬盘不行,主要靠地址总线,数据总线,控制总线)
-
定义
- 地址:内存单元的编号,从0开始的非负整数,范围:0–FFFFFFFFF(0–4G-1)(地址线是32位,刚好控制2的32次)
- 指针:指针就是地址,地址就是指针
指针变量是存放内存单元地址的变量
指针的本质是一个操作受限的非负整数(不能加乘除,只能减)
-
分类
- 基本类型的指针
基本概念
int i = 10; int *p = &i; // 等价于 int *p; 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;
- 举例:如何通过被调函数修改主调函数中一维数组的内容,通过两个参数:存放数组首元素的指针变量,存放数组元素长度的整型变量。
所有的指针变量只占4个字节,用第一个字节的地址表示整个变量的地址
结构体
- 为什么会出现结构体
为了表示一些复杂的数据,而普通的基本类型变量无法满足要求 - 什么叫结构体
结构体是用户根据实际需要自己定义的复合数据类型 - 如何使用结构体
两种方式:
struct Student st = {1000, "zhangsan", 20};
struct Student * pst = &st;
第一种:st.sid
第二种:pst->sid
pst所指向的结构体变量中的sid这个成员
4. 注意事项
- 结构体变量不能加减乘除,但可以相互赋值。
- 结构体变量和结构体指针变量作为函数传参的问题
动态内存的分配和释放
动态构造一维数组:假设动态构造一个int型数组
int *p = (int *) malloc (int len);
- malloc只有一个int型的形参,表示要求系统分配的字节数
- malloc函数的功能是请求系统len个字节的内存空间,如果请求分配成功,则返回第一个字节的地址,如果分配不成功,则返回NULL
- malloc函数能且只能返回第一个字节的地址,所以我们需要把这个无任何实际意义的第一个字节的地址(俗称干地址)转化为一个有实际意义的地址 ,因此malloc前面必须加(数据类型 * ),表示把这个无实际意义的第一个字节的地址转化为相应类型的地址。如:
int *p = (int *) malloc (50);
表示将系统分配好的50个字节的第一个字节的地址转化为int *型的地址,更准确的说是把第一个字节的地址转化为四个字节的地址,这样p就指向了第一个的四个字节,p+1就指向了第2个的四个字节,p+i就指向了第i+1个的4个字节。p[0]就是第一个元素,p[i]就是i+1个元素
double *p = (double * ) malloc (80);
表示将系统分配好的8 0个字节的第一个字节的地址转化为double *。C语言中,调用动态分配内存malloc函数后,必须手动释放(free释放)。
跨函数使用内存实例
# include <stdio.h>
int f();
int main(void)
{
int i = 10;
i = f();
printf("i = %d\n", i);
for (i=0; i<2000; ++i)
f();
return 0;
}
int f()
{
int j = 20;
return j;
}
模块一:线性结构
(把所有的结点用一根直线穿起来)
连续存储[数组]
- 什么叫数组
元素类型相同,大小相等 - 数组的优缺点
优点:存取速度很快
缺点:事先必须知道数组的长度,需要大块连续的内存块,插入删除元素很慢,空间通常是由限制的
离散存储[链表]
定义:
- n个节点离散分配
- 彼此通过指针相连
- 每个节点只有一个前驱节点,每个节点只有一个后续节点
- 首节点没有前驱节点,尾节点没有后续节点
专业术语:
- 首节点:第一个有效节点
- 尾节点:最后一个有效节点
- 头结点:头结点的数据类型和首节点类型一样,第一个有效节点之前的那个节点,头结点并不存放有效数据,加头结点的目的主要是为了方便对链表的操作
- 头指针:指向头结点的指针变量
- 尾指针:指向尾节点的指针变量
确定一个链表需要几个参数:
只需要一个参数:头指针
因为我们通过头指针可以推算出链表的其他所有参数
分类:
单链表
双链表:每一个节点有两个指针域
循环链表:能通过任何一个节点找到其他所有的结点
非循环链表
链表中每个结点的存储结构
typedef struct node
{
int data; //存储数据本身
struct node *pNext; //pNext指向一个和它本身存储指向下一个结点的指针
}NODE, *PNODE; //NODE等价于struct node,PNODE等价于struct node *
PNODE p = (PNODE) malloc (sizeof(NODE)); // 将动态分配的新节点的地址赋给p
free p; //删除p指向结点所占的内存,不是删除p本身所占内存
p->pNext; //p所指向结构体变量中的pNext成员本身
算法:
遍历
查找
清空
销毁
求长度
排序
删除节点
先临时定义一个指向p后面结点的指针r
r = p->pNext; //r指向p后面的那个结点
p->pNext = r->pNext;
free(r);
插入节点
把q所指向的结点插到p所指向的结点后面
- 方法一:先临时定义一个指向p后面结点的指针r
r = p->pNext; // r指向p后面的那个结点
p->pNext = q;
q->pNext = r;
- 方法二:
q->pNext = p->pNext;
p->pNext = q; //两行代码的顺序不可以倒过来
链表的优缺点:
- 优点:空间没有限制,插入删除元素很快
- 缺点:存取元素速度慢
线性结构的两种常见应用之一 栈
栈和队列是一种特殊的线性结构,是连续存储或离散存储的一种应用。
定义
一种可以实现“先进后出”的存储结构
栈类似于箱子
分类
- 静态栈
- 动态栈
算法
出栈
压栈
应用
函数调用
中断
表达式求值
内存分配
缓冲处理
迷宫
int main(void)
{
int p;
int * m = (int *)malloc(100);
}
// 如静态变量p和m是在栈中分配,有操作系统自动分配和释放。而(int *)malloc(100);执行后,将在堆中分配一块100字节的内存,由程序员手动分配。
栈的示意图
线性结构的两种常见应用之二 队列
定义
一种可以实现“先进先出”的存储结构
分类
-
链式队列 – 用链表实现(比较简单)
-
静态队列 – 用数组实现
静态队列通常都必须是循环队列
循环队列的讲解:
-
静态队列为什么必须是循环队列(减少对内存的浪费)
现在如果一个数组里面存了四个元素,那么front就指向第一个有效元素,而rear指向最后一个元素的下一个元素,当增加元素时,只能在rear一端增加,即rear向上移。删除元素时,只能在front一端删除元素,即front向上移。但是如果一直增增删删,那么就会造成rear端溢出,而front端浪费,所以对于这种情况,可以采用循环队列的形式,即当rear已经指向数组最后一个元素时,那么就可以转而将rear指向数组的第一个空出来的空间。 -
循环队列需要几个参数来确定
需要2个参数来确定:front 和 rear -
循环队列各个参数的含义
2个参数在不同场合有不同的含义- 队列初始化
front和rear的值都为零 - 队列非空
front代表的是队列的第一个元素
rear代表的是队列的最后一个有效元素的下一个元素 - 队列为空
front和real的值相等,但不一定为零
- 队列初始化
-
循环队列入队伪算法讲解
两步完成:- 将值存入rear所代表的位置
- 错误的写法 :rear = rear+1;
正确的写法是:rear = (rear+1)%数组的长度
-
循环队列出队伪算法讲解
front = (front + 1) % 数组的长度 -
如何判断循环队列是否为空
如果front与rear的值相等,则该队列就一定为空 -
如何判断循环队列是否已满
预备知识:front的值可能比rear大,front的值也可能比rear小,当然也可能两者相等
两种方式:
1. 多增加一个表标识参数
2. 少用一个元素(通常使用第二种方式)
如果r和f的值紧挨着,则队列已满。用C语言伪算法表示就是:
if ( (r+1)%数组长度 == f) 已满 else 不满
队列算法
入队
出队
队列的具体应用
所有和时间有关的操作都与队列有关
专题:递归
定义
一个函数自己直接或间接调用自己
函数的调用:
当在一个函数的运行期间调用另一个函数时,在运行被调函数之前,系统需要完成三件事:
- 将所有的实际参数,返回地址等信息传递给被调函数。
- 为被调函数的局部变量(也包括形参)分配存储空间
- 将控制转移到被调函数的入口
从被调函数返回主调函数之前,系统也要完成三件事:
- 保存被调函数的返回结果
- 释放被调函数所占的存储空间
- 依照被调函数保存的返回地址将控制转移到调用函数
当有多个函数相互调用时,按照“后调用先返回”的原则,上述函数之间信息传递和控制转移必须借助“栈”来实现,即系统将整个程序运行时所需的数据空间安排在一个栈中,每当调用一个函数时,将在栈顶分配一个存储区,进行压栈操作,每当一个函数退出时,就释放它的存储区,就进行出栈操作,当前运行的函数永远都在栈顶位置。
A函数调用A函数和A函数调用B函数在计算机看来是没有任何区别的,只不过用我们日常的思维方式比较怪异而已。
递归必须满足的三个条件:
- 递归必须得有一个明确的终止条件
- 该函数所处理的数据规模必须在递减
- 这个转化必须是可解的
递归和循环
- 递归:易于理解,速度慢,存储空间大
- 循环:不易理解,速度快,存储空间小
举例:
- 求阶乘
- 1+2+3+4+…100的和
- 汉诺塔
- 走迷宫
递归的应用:
树和森林就是以递归的方式定义的
树和图的很多算法都是以递归来实现的
很多数学公式就是以递归的方式来定义的:斐波拉契序列 1 2 3 5 8 13 21 34
模块二:非线性结构
树
树定义
专业定义
- 有且只有一个称为根的节点
- 有若干个互不相交的子树,这些子树本身也是一棵树
通俗的定义
- 树是由节点和边组成
- 每个节点只有一个父节点但可以有多个子节点
- 但有一个节点例外,该节点没有父节点,此节点称为根节点
专业术语
节点 父节点 子节点
子孙 堂兄弟
深度:从根节点到最底层节点的层数称之为深度,根节点是第一层
叶子节点:没有子节点的节点
非终端节点:实际就是非叶子节点
度:子节点的个数称为度
树分类
-
一般树:任意一个节点的子节点的个数都不受限制
-
二叉树:任意一个节点的子节点个数最多两个,且子节点的位置不可更改
分类:- 一般二叉树
- 满二叉树:在不增加树的层数的前提下,无法再多添加一个节点的二叉树就是满二叉树
- 完全二叉树:如果只是删除了满二叉树最底层最右边的连续若干个节点,这样形成的二叉树就是完全二叉树。
-
森林:n个互不相交的树的集合
树的存储
-
二叉树的存储
- 连续存储[完全二叉树]
优点:查找某个节点的父节点和子节点(也包括判断有没有子节点)方便快速
缺点:耗用内存空间过大 - 链式存储
优点:耗用内存空间小
缺点:查找父节点不方便
- 连续存储[完全二叉树]
-
一般树的存储
双亲表示法:求父节点方便
孩子表示法:求子节点方便
双亲孩子表示法:求父节点和子节点都很方便
二叉树表示法:把一个普通树转化成二叉树来存储
具体转换方法:设法保证任意一个节点的左指针域指向它的第一个孩子,右指针域指向它的下一个兄弟,只要能满足此条件,就可以把一个普通树转化成二叉树
一个普通树转化成的二叉树一定没有右子树 -
森林的存储
先把森林转化成二叉树,再存储二叉树
操作
遍历
-
先序遍历【先访问根节点】
先访问根节点,再先序访问左子树,再先序访问右子树
-
中序遍历【中间访问根节点】
先中序遍历左子树,再访问根节点,再中序遍历右子树
-
后序遍历【最后访问根节点】
先中序遍历左子树,再中序遍历右子树,再访问根节点
已知两种遍历序列求原始二叉树
-
先序,中序和后序三种遍历中,只知道其中任意一个,是无法还原其原始的树结构的。
-
通过先序和中序 或者 中序和后序 两种方式我们可以还原原始的二叉树。但是通过先序和后序是无法还原原始的二叉树的。
-
换种说法,只有通过先序和中序 或者 通过中序和后序我们才能唯一的确定一个二叉树的。
-
已知先序和中序求后序例子
示例1:
先序:ABCDEFGH(排在前面的为根节点)
中序:BDCEAFHG
求后序:DECBHGFA示例2:
先序:ABDGHCEFI
中序:GDHBAECIF
求后序:GHDBEIFCA -
已知后序和中序求后序例子
示例1:
中序:BDCE A FHG
后序:DECB HGF A(排在后面的为根节点)
求先序:A BCDE FGH
应用
树是数据库中数据组织的一种重要形式
操作系统父子进程的关系本身就是一棵树
面向对象语言中类的继承关系本身就是一棵树
赫夫曼树
图
模块三:查找和排序
折半查找
排序:
冒泡
插入
选择
快速排序
归并排序
排序和查找的关系
排序是查找的前提
排序是重点
Java中容器和数据结构相关知识
Iterator接口
Map 哈希表
再次讨论什么是数据结构
数据结构研究是数据的存储和数据的操作的一门学问
数据的存储分为两部分:
- 个体的存储
- 个体关系的存储
- 从某个角度而言,数据的存储最核心的就是个体关系的存储,个体的存储可以忽略不计
再次讨论到底什么是泛型
同一种逻辑结构,无论该逻辑结构物理存储是什么样子的
我们都可以对他执行相同的操作