三、数据结构
1.时间复杂度
重点:计算时间复杂度,就是算出每条语句的执行次数,每条语句按上图规则求出时间复杂度,然后复杂度相加,再取最高的。
嵌套循环,复杂度相乘。
2.渐进符号
O符号:渐进上界,大于等于算法运算次数
Ω符号:渐进下界,小于等于算法运算次数
渐进紧致界:渐进上界 与 渐进下界相等
3.递归的时间复杂度、空间复杂度
每次递归时间复杂度和空间复杂度不变的情况下:
- 递归的时间复杂度:递归的次数×每次递归的时间复杂度
- 递归的空间复杂度:递归的次数×每次递归的空间复杂度
递归时间复杂度本质也是求语句的运行次数,如果出现每次递归中循环语句循环次数会发生变化,就自己算,等比数列或者等差数列去算
4.递归式主方法
第三种不会考
- 先写出a,b,f(n)
- 然后利用公式求出 衣服新浪或k
- 然后带入公式
5.线性表
除了线性表的顺序存储查找的时间复杂度O(1)为1,其他都是最好时间复杂度O(1),最坏时间复杂度O(n) ,平均时间复杂度O(n)
-
线性表的顺序存储
- 插入 表长为n,可能移动n个元素,移动元素个数的期望:n/2
- 最好时间复杂度O(1),最坏时间复杂度O(n) ,平均时间复杂度O(n)
- 删除 表长为n,可能移动n-1个元素,移动元素个数的期望:(n-1)/2
- 最好时间复杂度O(1),最坏时间复杂度O(n) ,平均时间复杂度O(n)
- 查找:时间复杂度O(1)
- 插入 表长为n,可能移动n个元素,移动元素个数的期望:n/2
-
线性表的链式存储
-
单链表分为带头结点的和不带头结点的插入
-
带头结点的插入:插入的新节点为Node,i是第几个节点,P是指向第i个节点,要在k位置插入,则i要为k-1。步骤 我们找到插入位置的前一个节点P, Node->next = P->next;P->next = Node;
- 带头结点的插入时间复杂度:
- 最好时间复杂度O(1),最坏时间复杂度O(n) ,平均时间复杂度O(n)
-
不带头结点的插入:插入的新节点为Node,要判断k==1,因为当插入位置1的时候,P是第一个结点指针,只会插入到k的后面去,所以按照Node->next = p; p = Node;其他是Node->next = P->next;P->next = Node;
- 不带头结点的插入时间复杂度:
- 最好时间复杂度O(1),最坏时间复杂度O(n) ,平均时间复杂度O(n)
-
-
单链表分为带头结点的和不带头结点的删除
- 带头结点的删除:我们找到删除结点的前一个结点P,删除的结点为S ,S = P->next; P >next = S->next;
- 不带头结点的删除:我们找到删除结点的前一个结点P,删除的结点为S ,S = P->next; P >next = S->next;删除第一个结点比较特殊,直接将 第一个结点的指针赋给第二个结点
- 删除的时间复杂度:最好:O(1),最坏:O(n),平均:O(n)
-
单链表查找的时间复杂度 最好:O(1),最坏:O(n),平均:O(n)
-
-
循环单链表
- 循环单链表,查找,删除,插入的时间复杂度 最好:O(1),最坏:O(n),平均:O(n)
-
双链表
结点有前驱结点和后驱结点两个结点
看清题目,是尾指针,末尾加入尾结点
6.栈
先进后出
有顺序存储栈和单链表栈
7.队列
先进先出
- 循环队列
- 循环队列的题目,注意看头指针和尾指针的位置
- 有些尾指针指示队尾元素之后的位置
- 队列的链式存储
- 队尾(尾指针)入队,队头(头指针)出队。
- 入队不需要遍历整个链表,直接移动尾指针。
- 出队不需要遍历整个链表,直接移动头指针。
8.串
-
空串:长度为零的串称为空串,空串不包含任何字符。
-
子串:由串中任意长度的连续字符构成的序列称为子串。
-
空串是任意串的子串。
9.串的模式匹配与朴素模式匹配
k 为主串的第几个,i为开始比较后,i在主串的位置 ,每次比较开始 将 i = k,j为子串开始比较的位置 j=1
朴素模式匹配中:最原始的匹配方法,用子串和主串去一一比对,若不匹配,则从主串的下一个字符再去一一匹配,直到匹配结束。
- 最好时间复杂度:O(m)或者O(1),比较次数m次
- 最坏时间复杂度:O(n×m),匹配成功的最坏的比较次数 (n-m+1)次
- 平均时间复杂度:O(n+m),平均比较次数(n+m)/2次
10. KMP 匹配
-
手算next数组值
-
先遣知识:
- 串的前缀:包含第一个字符并且不包含最后一个字符的子串。
- 串的后缀:包含最后一个字符并且不包含第一个字符的子串
-
第i个字符的next值为,从1 ~ i - 1串中最长相等的前后缀长度+1。特殊情况下next[1] = 0; next[2] = 1;
-
注意:这个next数组是从1开始的,即第i个字符,数组中表示也是next[i],不是next[i-1]
-
-
算出next数组值之后,用子串和主串去一一比对,若不匹配,则从主串的下一个字符再去一一匹配,KMP算法做了个优化,模式串(子串)的第几个匹配不成功,则通过之前求出的next数组,获得相应的next数组值,然后模式串的j就为这个next数组值。
-
时间复杂度:O(n+m)
11.数组
-
一维数组
- L:元素大小 ,LOC:数组首地址
- 下标0开始,下标为i的元素的首地址 = LOC + i*L;
- 下标1开始,下标为i的元素的首地址 = LOC+ (i-1)*L;
-
二维数组
-
总共N行,i:行下标。总共M列,j:列下标。LOC:数组首地址,L:元素大小
-
下标从0开始
-
按行优先 位于第几个 :位置 = i×M+j
-
按列优先 位于第几个:位置 = j×N+i
-
-
下标从1开始
- 按行优先 位于第几个 :位置 = (i-1)×M+(j-1)
- 按列优先 位于第几个:位置 =(j-1)×N+ (i-1)
-
i == j的时候,按行存储和按列存储一样
-
12.矩阵
-
对称矩阵
- 对称矩阵中A[i,j] = A[j,i]
- 所以我们存值只用存对角线的一个三角区
-
三对角矩阵
- 二维数组下标0开始,位置 = 2i + j + 1
- 二维数组下标1开始 ,位置 = 2i + j -2
-
稀疏矩阵
- 用三元组表存储
- 第一个元素是i (行)
- 第二个元素是j (列)
- 第三个元素是数值
- 还可以用十字链表存储
13.树
1.定义
树结构是一种非线性结构,1对多(多是零个或多个)
结点为0则是空树
- 结点的度:拥有的孩子结点的数量
- 树的度:结点的最大度
- 叶子节点:没有子结点的结点
- 分支结点(内部结点):有分支的结点/不是根结点也不是叶子结点
- 兄弟结点:属于同一个父结点
- 层次:第几层,根结点就是第一层,根的孩子为第二层
- 树的高度:有几层
2.性质
-
**树中的结点总数等于树中所有结点的度数之和加1,**加1是加上根结点。
-
性质2:度为m的树中第i层上至多有m^(i-1)个结点(i≥1) 。
-
性质3:高度为h的m次树至多有(m^h-1)/(m-1)个结点
-
性质4:具有n个结点、度为m的树的最小高度为[logm(n(m -1)+1)]
-
上取整:整数+1 ,下取整:直接取整数 下取整大致长这样,L I
14.二叉树
- 满二叉树和完全二叉树
- 向下取整大致长这样,L I
- 二叉树性质4针对完全二叉树,可以由二 叉树性质2推导出来,算最小高度,直接结点数+1,然后log2 ,然后上取整
-
- 5个结点:42
15.二叉树存储
- 顺序存储
- 链式存储
二叉链表:n个结点有n-1个分支,每个结点有2个指针,总共2n个指针,n-1个有效指针,n+1个空指针域
三叉链表:n+2个空指针域
16.二叉树遍历和构造
- 二叉树先序遍历:根左右
- 二叉树中序遍历:左根右
- 二叉树后序遍历:左右根
- 根据遍历序列构造二叉树:通过先序、后序和层次获得根结点,然后再根据根结点在中序遍历中划分左右子树
17.平衡二叉树和二叉排序树
- 平衡二叉树:二叉树中的任意一个结点的左右子树高度之差的绝对值不超过
- 完全二叉树肯定是平衡二叉树
- 二叉排序树(二叉查找树):左子树所有结点的关键字小于根结点的关键字,右子树所有结点的关键字大于根结点的关键字,左右子树也是一颗二叉排序树(左小右大)
- 中序遍历得到的序列是有序序列,就是从小到大的排序序列
18.最优二叉树
- WPL = (叶子结点的权值)×(叶子结点到根的路径长度) 之和
- 如何构造最优二叉树
最优二叉树不唯一,其WPL值是唯一确定的
-
最优二叉树的性质
- 没有度为1的结点,全是0或2
- 树总结点个数 = 2n - 1 ;n为权值个数
-
软考中构造最优二叉树,这个很重要
-
哈夫曼编码:在最优二叉树上,左边是标0,右边是标1
-
哈夫曼编码压缩比:
- 等长编码的长度n,怎么求?:2^n >= 字符的种数
- 比如有a,b,c,d,e五种字符,则等长编码的长度为3,如果有七种字符,长度也是3
19.建立线索二叉树
就是一种数据结构,不是一种数的类型,任何树都可以是线索二叉树
20.图
- 有向图:每条边有方向
- 无向图:每条边没有方向,就是一条直线
- 完全图
- 无向完全图:n个顶点,而每一个顶点与其他n-1个顶点之间都有边。边的数目为n(n-1)/2
- 有向完全图:任意两个不同顶点之间都有方向相反的两条弧存在。弧的数目为n(n-1)
- 顶点的度:顶点v的度是指关联于该顶点的边的数目
- 出度 A----->B ,这就A的出度,从顶点A指出去的边
- 入度 A<-----B ,这就A的入度,从其他顶点指向A的边
- 度 = 出度 + 入度
- 总度数 = 边的数量(e)×2
- 总度数 = 2e
- 连通图和强连通图:
- 连通图:是无向图,任意两个顶点都是连通的
- 最少n-1条边,最多n(n-1)/2条边
- 强连通图:是有向图,任意两个顶点,从顶点v到顶点u和从顶点u到顶点v都存在路径
- 最少n条边,最多n(n-1)条边
- 连通图:是无向图,任意两个顶点都是连通的
20.图的存储结构
-
邻接矩阵
有向图是e个非零元素,无向图是2e个非零元素
-
邻接表
-
稠密图与稀疏图
- 稠密图:边多,用邻接矩阵存最好,不会浪费太多空间
- 稀疏图:边少,用邻接表最好
-
网:边或弧带权值的图称为网
21.图的遍历
-
定义:图的遍历是指从某个顶点出发,沿着某条搜索路径对图中的所有顶点进行访问且只访问一次的过程。
-
深度优先遍历(DFS)
- 这是递归的思想,只要能访问,就一直访问到不能访问为止。不能访问的话就回退到上一个顶点。
- 时间复杂度:深度优先,采用邻接矩阵的时间复杂度O(n^2)===========采用邻接表的时间复杂度O(n+e)
-
广度优先搜索(BFS)
- 访问当前顶点v1的所有邻接点后,再往下访问这些邻接点中的某一个顶点v2的所有邻接点,v2的所有邻接点访问完后,在访问v1所有邻接点中某一个邻接点v3的所有邻接点,以此类推
- 用队列理解就是,搜索到的顶点入队,当该顶点所有的邻接点都在队列中(即都被访问过),则该顶点出队,出队序列就是广度优先搜索的序列
- 时间复杂度:广度优先,采用邻接矩阵的时间复杂度O(n^2)===========采用邻接表的时间复杂度O(n+e)
-
拓扑排序:AOV网:有向无环图
-
对 AOV网进行拓扑排序的方法如下
- (1)在AOV网中选择一个入度为0(没有前驱)的顶点且输出它。
- (2)从网中删除该顶点及与该顶点有关的所有弧。
- (3)重复上述两步,直到网中不存在入度为0的顶点为止。
-
在有向无环图G的拓扑序列中,顶点vi在Vj之前,则可能存在弧<vi,vj>,一定不存在弧<vj,vi>。可能存在v到vj的路径,一定不存在vj到vi的路径
-
拓扑排序 序列:614325
22.普里姆算法和克鲁斯卡算法(生成最小生成树)
普里姆算法:加点法,在已加入的点中找到邻接点最短的边
- 时间复杂度O(n^2),n是顶点的数量
- 适用于稠密图
克鲁斯卡算法:加边法,直接找到权值小的边
这两个都不能形成环
- 时间复杂度O(mlogm),m是边的数量
- 适用于稀疏图
四、数据结构 - 排序
1.查找分类
-
静态查找表有:顺序查找,折半(二分)查找,分块查找
-
动态查找表有:二叉排序树,平衡二叉树,B_树,哈希表
-
平均查找长度ASL = PC之和,P是查找概率为1,C是查找元素比较的个数
2.查找方法
-
顺序查找
- 平均查找长度:ASL = (n+1)/2
- 顺序结构和链式结构都适用
- 时间复杂度 T = O(n)
-
折半查找
- 折半查找用于顺序存储,元素必须有序
- 二分查找的过程:
- 有1到12个数,从小到大排序,下标从1开始
- mid = (1+12)/2 = 6 下取整
- 和下标6比,比下标为6小,则high = mid -1;
- 则比1到5
- mid = (1+5)/2 = 3
- 最多比较的次数 :log2n下取整+1,平均查找长度:ASL = log2(n+1)-1
- 时间复杂度 T = O(log2n)
-
哈希表
-
计算地址:Hi = key%m
-
解决冲突:Hi = (H(key)+di )%m
- n个元素,长度m取接近n但不大于n的质数,可以减少冲突。哈希表的位置为0开始,0,1,2,3,……,m-2,m-1
-
a 是哈希表的装填因子=表中已装入的记录数 / 哈希表的长度,a越小,发生冲突的可能性就越小
-
例题:
-
3.小顶堆和大顶堆
优先队列使用堆结构
小顶堆:所有孩子结点都比父亲结点大
大顶堆:所有孩子结点都比父亲结点小
4.排序
排序:关键字满足以下递增(或递减)关系就是
若在待排序的一个序列中,Ri和Rj的关键字相同,即ki=kj,且在排序前Ri领先于Rj,那么在排序后,如果Ri和Rj的相对次序保持不变,Ri仍领先于Rj,则称此类排序方法为稳定的。若在排序后的序列中有可能出现Rj领先于Ri的情形,则称此类排序为不稳定的。
归位:确定了某个元素的位置,这个元素在之后排序结果中不会变化了
稳定:直接插入排序,冒泡排序,归并排序,基数排序(冒插归基)
归位:简单选择排序,堆排序,快速排序,冒泡排序(**快选冒归 **)
-
直接插入排序,元素基本有序,插入排序更适宜
- 就跟打牌中摸牌,理牌一样,从后往前比,大的放后面,小的放前面
-
希尔排序
- 先取一个小于(n的整数d1作为第一个增量,把文件的全部记录分成d1个组,即将所有距离为d1倍数序号的记录放在同一个组中,在各组内进行直接插入排序;然后取第二个增量d2;(d1<d2),重复上述分组和排序工作,依此类推,直到所取的增量 d=l(di<di-1<…<d2<d1),即所有记录放在同一组进行直接插入排序为止。
-
简单选择排序
- 从小到大:第一次排序就把最小的元素得到,放在第一位,然后第二次排序就在剩下的元素中继续找最小的元素,以此重复直到排序结束
-
堆排序
- 大顶堆就确定了最大的元素,把最大元素移出树后,将树中最后一个结点移至根结点,然后调整为新堆
-
冒泡排序
- 比较数组中,两个相邻的元素,如果第一个数比第二个数大,就交换他们的位置,每轮比较都会产生出一个最大或者最小的数, 下一轮则少一次排序
//冒泡排序 //1. 比较数组中,两个相邻的元素,如果第一个数比第二个数大,就交换他们的位置 //2. 每轮比较都会产生出一个最大或者最小的数 //3. 下一轮则少一次排序 // 2 3 5 4 1 9 8 5 -1 第一次比较2 3 // 2 3 5 4 1 9 8 5 -1 第二次比较3 5 // 2 3 4 5 1 9 8 5 -1 第三次比较5 4 // 2 3 4 1 5 9 8 5 -1 // 2 3 4 1 5 9 8 5 -1 // 2 3 4 1 5 8 9 5 -1 // 2 3 4 1 5 8 5 9 -1 // 2 3 4 1 5 8 5 -1 9 //这是一轮 public static int[] sort(int[] array){ for (int i = 0; i < array.length-1; i++) { for(int j = 0;j<array.length-1-i;j++){ if (array[j]>array[j+1]){ int t = array[j]; array[j] = array[j+1]; array[j+1] = t; } } } return array; }
-
快速排序:元素基本有序是最坏的情况
- 先从右向左找第一个小于x的数,找到之后与下标i交换,然后从左向右找第一个大于x的数,与下标j
void quick_sort(int a[], int l, int r) //l是low,低的一边,右边第一个元素的下标 { if (l < r) { int i,j,x; i = l; j = r; x = a[i]; while (i < j) { while(i < j && a[j] > x)// 先从右向左找第一个小于x的数 j--; //没找到则j--,找下一个 a[i] = a[j]; //从右向左找到比x小的数之后,与下标i交换 while(i < j && a[i] < x)// 从左向右找第一个大于x的数 i++; //没找到则j--,找下一个 a[j] = a[i];//从左向右找到比x小的数之后,与下标j交换 } a[i] = x; quick_sort(a, l, i-1); //枢轴的左边继续快速排序 quick_sort(a, i+1, r); //枢轴的右边继续快速排序 } }
注意:快速排序如果取中间的元素为枢轴,j从后往前找第一个大于枢轴的元素。i从前往后找第一个小于枢轴的元素,然后这两个元素交换
-
归并排序
- 先分,就是将所有元素分成一个个元素,再合,将所有元素按顺序合起来。怎么合?举例子:指针第一位i在4578,另一个指针j在1236。4和1比,4比1大,1放在另一个备用数组的第一位,然后j++,再4和2比,则2放在用数组的第二位,j++。以此类推