如何更好的编程,系统性的编程?
如何学好数据结构:
1. 代码量 较多 多动手写
数据结构与算法:
数据结构: 描述 从现实中 抽象出 数据模型 即 数据关系的抽象
逻辑结构: 数据之间的 逻辑关系
表: 线性结构 队列,栈, 数据之间是线性关系 一一对应
树: 非线性结构 1对多
图: 非线性结构 多对多
存储结构: 对数据 以及 数据关系的 存储
连续存储: 将所有数据放到一起 数组
离散存储: 数据可能分布在内存的 不同位置
对数的操作(算法):
不是简单的数值操作 而是一些 逻辑操作
增: 增加数据
删: 删除数据
改: 修改数据
查: 查找或遍历数据
算法: 就是对 某个动作的 操作流程 或步骤
算法特性:
1. 可行性 可以实现 每个计算步骤能够在有限的时间内完成
2. 确定性 没有歧义 步骤唯一且确定
3. 有穷性 有明确的目标且可到达 步骤是有限的
4. 有一个或多个输入
5. 有一个或多个输出
求两正整数m、n的最大公因子的算法如下:
① 输入m,n;
② m/n(整除),余数→r (0≤r≤n);
③ 若r=0,则当前n=结果,输出n,算法停止;否则,转④;
④ n→m,r->n; 转②。
如初始输入 m=10,n=4,则m,n,r 在算法中的变化如下:
m n r
10 4 2
4 2 0(停止)
即10和4 的最大公因子为2。
余
m n r
20 15 5
15 5 0
r
15 20 15
20 15
求 1+2+3+...n = ???
算法1: 语句频度T 量级 O
for(i=1;i<=n;i++) sum+= i; n+1 O(n)
算法2:
sum = (1+n)*n/2 1 O(c) 常数量级
算法优劣评判标准:
时间复杂度: 语句执行的频度 与算法程序执行的时间 没有必然关系
执行的频度: 算法中 语句执行的次数 与 输入问题规模的 比值
度量一个算法的时间复杂度 通常使用 其 量级表示
冒泡排序: 语句频度
for(i=1;i<n;i++) n
for(j=1; j<n-i-1; j++) (n)(n / 2)
if( a[j] > a[j-1] ) n(n/2)
{交换a[j] a[j-1]}
n^2 + n 量级 O(n^2)
常见的 时间复杂度的 量级
最好的 O(1) 常量级 算法语句执行次数 与 输入问题规模 无关
较好
O(log n) 对数级
O(n) 线性级
一般 O(n*log n)
较差 O(n^2) 平方级
差 O(2^n) 次方级
O(n!) 阶乘级
空间复杂度: 占用内存的多少
交换两个数
杨辉三角:第i行j列的值
A(i,j) = i!/(j!*(i-j)!)
常见数据数据:
1. 表结构 线性结构
数据逻辑组织为表形式,逻辑上,数据是连续排列的
每个节点(除头尾外),都有且仅有一个 前驱 和 一个后继
头节点 只有后继 没有前驱
尾节点 只有前驱 没有后继
节点: 数据逻辑的最小单元
C中 数组 就是 表结构
存储结构:
顺序存储: 数组存储
顺序表 :存储上连续存储,逻辑上是表结构
离散存储: 链式存储
链表: 存储上使用离散存储, 逻辑上是表结构
顺序表:
C语言程序模型:
表结构定义:
//1 定义节点类型 假设为int
typedef int Data_t;
//2 定义顺序表 结构
//动态长度
typedef struct list_t
{
int max_len; //存储顺序表的最大长度
int cnt; //存储当前表中节点个数
Data_t *data;//存储节点的数组动态分配长度
};
//固定长度
define MAXLEN 20
typedef struct list_t
{
int cnt; //存储当前表中节点个数
Data_t data[MAXLEN]; //静态分配数组长度
};
struct list_t list;
顺序表的操作:
创建表: 动态长度表为例
int create_list(struct list_t *head ,int max_len);
销毁表
int del_list(struct list_t *head);
...
增: 增加节点 插入数据
int insert_list(struct list_t *head, int index, Data_t data);
作业 删: 删除节点
改: 修改节点值
查: 遍历 获取某个节点数据
判空
判满
获得长度
表A += 表B: 将b表中的元素 添加A表末尾;
....
预习: 链表
8月11号
表的 离散存储 链表 :
逻辑结构 表结构 通过指针方式实现 逻辑上的链接
//1. 定义数据类型 假设为int
typedef int Data_t;
//2. 定义节点类型
typedef struct node_t
{
Data_t data; //数据域:存储数据本身的
struct node_t *next;//指针域:存储逻辑关系
}node_t;
链表中的一些情况的表示:
链表结尾节点 表示: 指针域 == NULL
空链表 表示 : 使用一个 不存储数据的 节点 作为头节点.
当头节点的 指针域 == NULL 表示空链表
创建空链表:
//返回值 失败返回 NULL
//成功 返回 链表头节点地址
node_t * create_link_list(void);
插入数据:
int insert_data(node_t *head, int index, Data_t data);
删除数据:
1.给定一个下标, 删除该节点
int del_data(node_t *head, int index);
2.给定节点的地址, 删除该节点
作业:int del_data_byaddr(node_t *head, node_t* delp);
改:
修改下标位置的值
作业:int change_data(node_t* head, int index , Data_t data);
链表拼接 :
int connect_link_list(node_t* head1, node_t* head2);
链表翻转: 倒叙
int link_list_Reverse(node_t* head);
1. 头 空链表 身体
2. 遍历身体 头插法 到头中
作业:
查:
给定一个下标 index 给出其 节点地址或值
给定一个数据data 查询该数据 所在的节点下标 或 节点地址
链表与顺序表的 优劣:
链表根据节点长度动态分配,没有长度限制
链表的插入和删除节点,时间复杂度 比顺序表优秀 不会存在大片数据整体移动的情况
顺序表:
存储密度高 不会有指针域 浪费空间
顺序表的 随机访问效率高
链表应用:
约瑟夫环问题:
参考示例 约瑟夫环问题.c
静态链表: 链表的节点 事先已经开辟了(节点数组), 动态下标或指针域来了 建立逻辑关系
队列: 是一种特殊的线性表
规定了 表的入口和出口 同时 先进先出
多用于 缓冲
队列的实现:
顺序存储的队列: 循环队列
离散存储的队列: 链式队列
离散方法的链式队列: 双向循环链表
作业: 参考 模型图
循环队列:实现
1. 创建空队列
2. 销毁队列
3. 入队
4. 出队
5. 置空队列
6. 判空判满
7. 获得队列长度
栈:
一种特殊 的 线性表,
只能在表的 一端 进行插入和删除
后进先出
顺序栈 : 数组 从数组尾部进行插入和删除
链式栈 : 链表 从头部插入和删除
顺序栈:
4种类型的栈:
增栈 : 入栈时 栈顶指针 向大地址方向移动
减栈 : 入栈时 栈顶指针 向小地址方向移动
空栈 : 栈顶指针 指向的位置数据 无效(空)
满栈 : 栈顶指针 指向的位置数据 有效(满)
空增栈:
满增栈:
空减栈:
满减栈:
哈希表: 不是线性表 方便查找而设计的
门牌号: 407
张三
张 z --> s --> 顺序查找
章 z
链表的思考题:
已知一个单向链表的某个节点地址 p 如何判断 这个链表中是否有环
使用一个辅助表, 将变列过的节点地址 放入该表
每次移动节点指针p时 对该表进行比较 若p出现在该表中 则存在环
表 去除重复数据
已知一个单向链表的某个节点地址 p p不是最后一个节点
问 在不知道头节点地址的 情况下 能否删除该节点 若能 什么方法?
将后一个节点数据 移动到 p节点数据域 p->data = p->next->data;
备份q q = p->next;
修改指向 p->nxext = q->next;
释放 p的后一个节点 free(q);
树:
一个节点 有0个或多个 直接后继 节点
有1个(子节点)或 0个(根节点) 直接前驱
不能有环结构
目录结构 典型数结构
根节点: 树的起始位置 一棵树 一定有根节点
子节点: 树结构中 除根节点外 其他节点都称作 子节点
叶子节点: 特殊的子节点, 没有后继的 子节点
树的度: 以树中 直接后继最多的节点 个数 称该树的 度
树的深度: 从根开始 到每一个叶子节点 最长的经过的节点 个数
即树的层数
树的路径: 从根节点开始 到目标节点 经过的节点顺序
子树: 一颗树 中的 一部分节点 构成的一颗 树 称该树是 其子树
数据结构中 主要研究 二叉树:
二叉树: 一个度为2的树
1. 二叉树 有0个或1个或2个 直接后继节点 且 严格区分左右 即使只有一个子节点也要区分左右。
2. 一个二叉树 其 子树 也一定是二叉树
二叉树(Binary Tree )是n(n≥0)个节点的有限集合,它或者是空集(n=0),
或者是由一个根节点以及两棵互不相交的、分别称为左子树和右子树的二叉树组成。
二叉树与普通有序树不同,二叉树严格区分左孩子和右孩子,
即使只有一个子节点也要区分左右。
二叉树的特例:
满二叉树: 除叶子节点外 其他有所节点 都有两个 直接后继 且 叶子节点都在最后一层
完全二叉树: 只有最下面两层有度数小于2的节点,
且最下面一层的叶节点集中在最左边的若干位置上。
除最底层 没有拍满外 其他层 均 盘满 且 每一层都需要按照先左后右的顺序排列
带权二叉树:
二叉树的 路径上 带有 权值
应用:
3+3-5*0
二叉树的一些特性:
二叉树的性质:
1.在二叉树的第i层上至多有2的i-1次方个结点 2^(n-1)
2.深度为k的二叉树至多有2的k次方-1个结点 2^k -1
3.对任何一棵二叉树T,如果其终端结点数为n0,度为2的结点n2,那么n0=n2+1
叶子节点的 个数 始终比 度为2的节点个数多 1个
4.具有n个结点的完全二叉树的深度为 = (log2n)+1 取整数
第n号节点 是 (log2n)+1 取整 行
5.对一棵有n个结点的完全二叉树的结点按层序编号(从第一层到最后一层,每层从左至右),
对任一结点i(1<i<n)有:
a.如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点i/2 取整;
b.如果2i>n,则结点i无左孩子(即为叶子结点);否则其左孩子结点是2i;(偶数个结点,最后一个叶子结点一定是左)
c.如果2i+1>n,则结点i无右孩子;否则其右孩子是结点2i+1;(奇数个结点,最后一个叶子结点一定是右)
二叉树的存储:
1. 顺序存储: 将二叉树 补全为 完全二叉树 按 从左到右 从上到下
依次进行编号, 这个编号即数组下标
2. 离散存储:
构造节点:
二叉树的遍历:
1. 层次遍历 广度遍历
使用一个临时队列 辅助
2. 深度遍历
先序遍历 先根 后 左右 判断根节点 前
中序遍历 先左 根 右 判断左右子树 中
后序遍历 先左 右 根 判断根节点 后
先序或后序遍历 可以确定根节点 先序第一个就是根节点, 后续最后一个是根节点
中序遍历 可以确定左右子树
练习:
参考示例代码 tree.c
创建
删除 树
遍历
思考:
查找 节点
改节点值 给节点地址
插入节点 给节点地址 l r 要插入的数据
删除节点
作业:
1. 已知一个二叉树
中序遍历:B D C G F A H I
后序遍历:D G F C B I H A
画出这个二叉树结构
求先序遍历:
思考:
二叉树的层次遍历
二叉树的一些应用:
赫夫曼(Huffman)树,又称最优树,是带权路径长度最短的树
情景: "helloworld" 通过电报发送 问题 如何编码 可以使得这句话 发送的01最少
等长编码:
ASCII: 10字节 80bit
4bit编码:
h:0000
e:0001
l:0010
....
d:
40bit
变长编码: 二叉树
赫夫曼编码 27bit
图: 网结构
是一种比表更复杂的数据结构
树 是 特殊的图
图 有多个直接前驱 有多个 直接后继
专业术语:
顶点: 图(网)中 存储数据的节点
边/弧: 顶点与顶点间的关系 没有方向称边 有方向称弧
边/弧的权值: 从一个顶点到另一个顶点的代价
子图: 图中的一部分节点以及这些节点的边 够成子图
路径:从一个顶点到另一顶点所经过的边
简单路径:路径中所经过的顶点没有重复的
有向图: 边有方向
无向图: 边没有方向
有环图:有路径构成环
无环图:没有路径可以构成环
带权图: 边有权值
无权图: 边没有权值
连通图: 任意两个顶点都有路径可以到达
非连通图: 如图所示
一个连通图 有n个节点 有n(n-1)/2 边 有n(n-1)条弧
图的存储:
顺序存储: 节点: 数组 边: 邻接矩阵
对稠密图 或 节点较少的图
消耗内存较多
离散存储: 邻接链表
十字链表
稠密图: 边较多
稀疏图: 边较少
完全图: 边 = n(n-1) 或 n(n-1)/2 有所有的边
图解决实际问题:
寻路算法: 最短路径 Dijkstra算法
常见查找与排序算法:
索引查找: 目录 给数据设计一个目录 先查目录
哈希查找: 将目标 进行哈希, 得到哈希值 即存储位置
数据有序时:
二分法查找:
arr = {1,2,5,7,9,11,17,20,30,55,97};
(len-1)/2
查20的下标:
写代码:
int find(int data, int*arr, int start, int end);
//二分查找方式2
//data:要找的数
//arr :数组
//len :数组长度
//返回值: NULL没有找到 非空找到目标的地址
int * find2(int data, int *arr, int len)
{
if(len <= 0) return NULL;
int * p = arr + len / 2;
if(*p == data) return p;
if(*p > data) return find2(data,arr, p - arr );
else return find2(data, p+1, len - (p-arr)-1 );
}