数据结构
大O表示法
时间复杂度
空间复杂度
渐进符号
递归时间复杂度和空间复杂度
在每次递归的时间复杂度变化的情况下:
跟递归的次数相关,(展开式次数求和)
每次递归的时间复杂度求和,直到递归结束;
递归的空间复杂度:
每次递归的空间复杂度求和,直到递归结束;
主方法(渐进紧致界)
线性结构与线性表的定义
线性表的插入和删除
顺序表code
顺序表的初始化init()
//顺序表的源-数组
int[] array;//数组
int arrayLength ;//表长
final int N = 10 ;//容量假设为10
void init(){
array = new int[N];
//初始化数组给数组赋5个值
for (int i = 0; i < N/2; i++) {
array[i]=i+1;//赋值为1,2,3,4,5
}
arrayLength = N/2;
for (int i = 0; i < arrayLength; i++) {
System.out.println(array[i]);
}
}
顺序表的插入insert()
void insert(int k,int num){
System.out.println();
//1,首先判断插入元素的位置是否超过数组(顺序表)的长度;
if(k>array.length) {//如果超出数组元素的长度,这里是
//1,先将数组扩容
arrayLength++;
//2,将k赋值为array.length
k=arrayLength;
//示例
//1,假设要在第顺序表的三个位置,插入元素10
// int x = 3 ;
//2,先遍历顺序表,将从末尾的元素开始,逐个移动到后面,最后将要插入的元素插入到顺序表对应的位置
//3,判断当插入位置为第一个位置时的情况,还是需要先将末尾的元素开始,逐个移动到后面;
//4,判断是否插入的元素为最后一个元素,最后一个元素在插入时不需要移动数组,只需将数组扩容,并插入到最后即可
for (int i = arrayLength; i >= k - 1; i--) {
//先判断插入的位置,因为插入位置前面的元素不需要移动
if (k > arrayLength) {
array[arrayLength - 1] = num;
for (int j = 0; j < arrayLength; j++) {//注意:这里遍历的是数组的容量,而不是数组的长度length
System.out.print(array[j] + " ");
}
return;
}
if (i - 1 >= 0) {//判断是否数组越界
array[i] = array[i - 1];
}
}
array[k - 1] = num;
for (int i = 0; i < arrayLength; i++) {//注意:这里遍历的是数组的容量,而不是数组的长度length
System.out.print(array[i] + " ");
}
}
}
顺序表的删除delete()
void delete(int k ){
System.out.println();
//1,先判断删除数据的位置是否越界
if(k>=arrayLength) {
arrayLength--;
k=arrayLength;
//2,先考虑是否删除的是末尾的元素->不需要移动数组
for (int i = 0; i < arrayLength; i++) {
System.out.print(array[i]+" ");
}
}
//3,如果不是末尾数组元素,将该位置的元素删除的步骤
//4,遍历该位置的后面的元素,将逐个元素往前移动
if(k<arrayLength){
for (int i = k-1; i <arrayLength-1 ; i++) {
array[i]=array[i+1];//将后面的元素赋值给前面
}
arrayLength--;
for (int i = 0; i < arrayLength; i++) {
System.out.print(array[i]+" ");
}
}
}
顺序表插入或删除的时间复杂度
顺序表查找的时间复杂度
查找-》顺序表(使用类数组)->用下表随机存取
链表
单链表code
结点Node
public class Node {
int data ;//数据域
Node next ;//指针域
public Node() {
}
public Node(int data) {
this.data = data;
}
}
带头结点的单链表
public class HeadLinkList {
//带头结点的单链表
Node head ;
//初始化单链表
void init(){
head = new Node();
Node node1= new Node(1);
Node node2 = new Node(2);
Node node3 = new Node(3);
//将链表节点链接起来
head.next=node1;
node1.next=node2;
node2.next=node3;
}
void printList(){
Node p = head.next ;//用p来循环迭代list,来输出打印list中的数据域
while (p!=null){
System.out.print(p.data + " ");
p=p.next;
}
System.out.print("\n");
}
}
不带头结点的单链表
public class LinkList {
Node list ;
//初始化单链表
void init(){
Node node1= new Node(1);
Node node2 = new Node(2);
Node node3 = new Node(3);
//将链表节点链接起来
list=node1;
node1.next=node2;
node2.next=node3;
}
void printList(){
Node p = list ;//用p来循环迭代list,来输出打印list中的数据域
while (p!=null){
System.out.print(p.data + " ");
p=p.next;
}
System.out.print("\n");
}
}
带头结点的单链表的插入
boolean insert(int k ,Node node){
// //带头结点的单链表
// Node head ;
// //插入时遍历链表的位置
// int i = 0 ;//表示第一个节点,这里是指头结点
//先判断插入节点位置是否越界
if(k<1){return false;}
//在第k个位置之前插入新的结点node
//因为结点之间是链接起来的,不想数组可以随机存取,需要循环遍历链表才能找到需要插入节点的前后位置
Node p = head;//用p来循环迭代list
while (i<k-1&&p!=null){//注:p!=null是为了防止链表越界
i++;
p=p.next;//遍历找到要插入节点位置的前一个节点
}
if(p==null){return false;}//这里是再次判断,遍历后的结点是否为空
node.next = p.next ;//这里是将插入节点的后继赋值为迭代后插入位置前的(后继位置的next的结点)
p.next = node ;//将插入节点位置前的next的结点赋值为node,即是插入位置前的结点的后继为当前插入节点
return true ;
}
带头结点的单链表插入当表头存储的是链表长度时
boolean insert(int k ,Node node){
// //带头结点的单链表
// Node head ;
// //插入时遍历链表的位置
// int i = 0 ;//表示第一个节点,这里是指头结点
//先判断插入节点位置是否越界
if(k<1||k>head.data+1){return false;}
//在第k个位置之前插入新的结点node
//因为结点之间是链接起来的,不想数组可以随机存取,需要循环遍历链表才能找到需要插入节点的前后位置
int i = 0 ;//表示第一个节点,这里是指头结点
Node p = head;//用p来循环迭代list
while (i<k-1){//注:p!=null是为了防止链表越界
i++;
p=p.next;//遍历找到要插入节点位置的前一个节点
}
if(p==null){return false;}//这里是再次判断,遍历后的结点是否为空
node.next = p.next ;//这里是将插入节点的后继赋值为迭代后插入位置前的(后继位置的next的结点)
p.next = node ;//将插入节点位置前的next的结点赋值为node,即是插入位置前的结点的后继为当前插入节点
head.data ++;
return true ;
}
不带头结点的单链表的插入
boolean insert(int k ,Node node){
//不带头结点的插入,
//1,在第k个位置前插入
//2,需要找到第k个位置前的结点所在的位置,即找到第k个位置前所在的结点
//3,考虑特殊情况,当插入节点的位置为负数,或插入节点的位置超过单链表的长度时
//4,考虑特殊情况,当插入节点的位置为第一个节点时,无法找到插入节点的前一个节点的位置即k-1=0(当前节点在没有头结点的单链表中不存在)
//定义一个迭代结点,来代表链表节点中的位置
Node p = list ;//将第一个节点赋值给p
int i = 1;//代表第一个节点的位置
if(k==1){
node.next=list;//插入节点的后继结点为以前链表的第一个节点
list=node;//将新链表的第一个节点赋值给node
return true;
}
if(k<1)return false ;
while (i<k-1&&p!=null){
//当p不为空时迭代p,找到插入节点前的结点,k-1代表插入节点前的结点所在的位置
p=p.next;
i++;
}
if(p==null)return false;//判断插入节点前的位置是否右越界
//假设为下列这样插入
node.next=p.next;//插入接电的下一个位置为,找到插入节点位置的前一个节点的后继结点
p.next=node;//success//找到插入节点位置的前一个节点的后继结点(赋值)为插入节点
// //如果为这样
// p.next=node;
// node.next=p.next;//这句话如果执行前一段代码翻译->node.next=node//error
return true;
}
单链表插入时间复杂度
带头节点和不带头结点的单链表删除Code
带头节点的delete()Code
boolean delete(int k ){
//删除第k个节点
//1,先判断删除节点的位置是否合法
//2,遍历整个单链表找到删除节点的位置
int i = 0 ;//这个代表头结点的位置,用来遍历整个链表
Node p = head ;//迭代单链表
Node s ;
if(k<1||k>head.data){
//head.data代表单链表的长度
return false;
}
while (i<k-1){
i++;
p=p.next;
}
if (p==null)return false;
s=p.next;
p.next=s.next;
return true;
}
不带头节点的delete()Code
boolean delete(int k ){
//删除第k个节点
//1,先判断删除节点的位置是否合法
//2,遍历整个单链表找到删除节点的位置
int i = 1 ;//这个代表头结点的位置,用来遍历整个链表
Node p = list ;//迭代单链表
Node s ;
if(k<1){
//head.data代表单链表的长度
return false;
}
if(k==1){
list=list.next;//删除第一个节点,将第一个节点的后继赋值给第一个节点
}
while (i<k-1&&p!=null){
//因为不带头结点的单链表在删除第一个节点(k=1)和第二个节点时(k=2)
//i<k-1都不成立
//无法删除第一个节点
i++;
p=p.next;
}
if (p==null)return false;
s=p.next;
p.next=s.next;
return true;
}
单链表删除及时间复杂度
单链表查找及时间复杂度
单链表获取第k个节点Code
Node GetNode(int k ){
//获取第k个节点
if(k<0||k>head.data)return null;
int i = 1 ;//不获取头结点,从第一个节点开始遍历
Node p = head.next;
while (i<k){
i++;
p= p.next;
}
return p;
}
循环链表
循环链表与带头结点单链表的区别
head.next=head;
循环链表没有空的情况,在遍历时判断当是否为头结点时即一圈遍历完成;
尾指针tail(便于尾部插入)
双链表
结点中多一个属性pre,即一个节点有三个属性,(data,pre,next)
栈
顺序栈
类似顺序表->即数组[数组头为栈底,数组尾为栈顶]
特性:先进后出,只在栈顶插入删除
链栈
共享栈(顺序存储)
top1移动
top2移动
top1和top2同时移动直到栈的空间用完
队列
队的顺序存储结构
循环队列
队列的链式存储
双端队列
串
串的模式匹配
朴素匹配
*手算next数组
KMP模式匹配
使用next数组值,前后缀相等,来匹配下一个位置,来移动模式串的下表,继而减少移动主串的回退,、。
了解代码
一维数组
二维数组
按行优先
按列优先
当行和列相等时,按行存储和按列存储偏移量相等;
矩阵
对称矩阵
下三角区域i>j;行下标>列下标
对角线i=j;行下标=列下标
上三角区域i<j;行下标<列下标
下图为按行存储(下三角区域加对角线)或(上三角区域加对角线)a[i] [j] (矩阵中的元素位置)=a[k](按一维数组存储的位置)
三对角矩阵
A[i] [j] = A[k]<->稀疏矩阵中的元素按一维数组存储的方式,各元素对应的一维数组的元素;
2i+j+1推导过程:
这里有个问题?
如图最大是A[4] [4] 首尾元素个数都为2,中间部分元素个数为3;
当扩大时首尾元素个数是否会扩大?中间部分元素个数是否会扩大呢?
以A[2] [3] 定位,找前面有多少个元素;
*除了第一行是两个元素外,其他行都是三个元素,
A[i] [j] 先按(ix3-1)即是都按每行3个元素计算,最后减去一个(即是第一行只有两个元素);
*再看A[i] [j] 这行前面有多少个元素;
前面两个 A[2] [3] -> j-1 = 2 (满足条件);但是 A[1] [2] -> j-1 = 1(不满足条件)
*根据A[i] [j] 这行 -> 示例:A[2] [3] 这行的排位
A[2] [1] ,A[2] [2] ,A[2] [3] ->他们这三个元素按这行从左往右分别对应,第一个即该行元素1 A[2] [1] ,第二个即该行元素2 A[2] [2] ,第三个即该行元素3 A[2] [3] 。除第一行每行的排位顺序都一致,都满足每行3个元素的 j-i 从左往右 依次为
-1 , 0 ,1 ;让他们按1,2,3排位则需要将 j-i+2 ;
所以三对角矩阵中元素对应一维矩阵中元素的公式:
a[i] [j] = a[ix3-1+j-i+2] 即 a[i] [j] = a[2i+j+1] ;
稀疏矩阵
三元组顺序表,十字链表是对稀疏矩阵的压缩存储方式;
树
树的定义
树的基本概念
度为m的树?
该树中结点的度数的max=度为max的数;
即该树中结点的度数的max=度为m的树;
m次树=度为m的数;
树的性质
树的节点总数
树的结点总数=树中所有结点的度数之和加1(加1即加上根节点);
度为m的树中第i层的结点总数
**高度为h,度为m的树中至多的结点总数/ **
具有n个节点,度为m的树的最小高度
当结点的个数限制为n时,每层的结点的度都为m时,树的高度压缩最小;
公式转换
二叉树
二叉树的定义
二叉树的度恒定为2(只有左子树和右子树);
二叉树的性质
注意:第i层二叉树结点的个数; 高度为h的二叉树的至多节点个数的公式;
二者区别:公式推导?
第i层二叉树结点的个数-第一层为一个,指数为0;
高度为h的二叉树的至多节点个数的公式,等比为2的求和公式推导;
二叉树中叶子结点与度为2的节点的关系
N0=N2+1;
满二叉树;完全二叉树;非完全二叉树;
满二叉树:每层结点没有空缺(每个结点度都为2),都为满;
完全二叉树:除了最后一层,前面每层结点都为满(每个结点度都为2),最后一层结点必须从左叶子至右子树,(最后一层不为满);
非完全二叉树:
除了最后一层,前面每层结点都为满(每个结点度都为2),最后一层结点非必须从左叶子至右子树,(最后一层不为满);
具有n个节点完全二叉树的高度
满二叉树求层次(高度)
[log]向下取整;
注意满二叉树求节点所在层次的公式:(+1->根节点?)
具有n个节点,求二叉树的种类(卡特兰数)
二叉树顺序存储
在最坏的情况下,一个深度为k且只有k个结点的二叉树(单枝树-除了叶子节点外,其余节点度都为1的树)需要的存储单元?
二叉树链式存储
二叉链表-一对多(指针域中(存储左子树的根和右子树的根));
二叉链表n个结点的空指针域?
n个结点->2n个指针域(即存储左子树的根和右子树的根);
n个结点->n-1个有效指针域(-1即是减去根节点) ->n-1个分支(即是n-1个孩子结点);
二叉链表空指针域=总指针域-有效指针域 = n +1 ;
三叉链表-一对多(指针域中(存储左子树的根和右子树的根,父结点的信息));
三叉链表n个结点的空指针域?
n个结点->3n个指针域(即存储左子树的根和右子树的根,父结点的信息);
n个结点->n-1个子树有效指针域(-1即是减去根节点) ->n-1个分支(即是n-1个孩子结点);n-1个父节点有效指针域(一个节点即是儿子也是父亲)
三叉链表空指针域=总指针域-有效指针域 = n +2 ;
二叉树的遍历
先序遍历
中序遍历
后序遍历
层次遍历
根据序列构造二叉树
先序中序构造二叉树
中序后序构造二叉树
中序层序构造二叉树
平衡二叉树
注意:左右子树的高度差,依次往下
二叉排序树
二叉排序树定义
二叉排序树的构造
最优二叉树
什么是最优二叉树或叫做哈夫曼树?
pind.com解惑:
哈夫曼树(Huffman Tree),也称为最优二叉树,是一种特殊的二叉树,它的特性是树的带权路径长度达到最小zhuanlan.zhihu.com。
首先,来解释一下哈夫曼树中的几个关键概念:
-
路径和路径长度:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为1,则从根结点到第L层结点的路径长度为L-1zhuanlan.zhihu.com。
-
节点的权和带权路径长度:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积zhuanlan.zhihu.com。
-
树的带权路径长度:树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPLzhuanlan.zhihu.com。
构造哈夫曼树的步骤如下:
- 将待构造的哈夫曼树的n个权值看作是有n棵树的森林(每棵树仅有一个结点);
- 在森林中选出两个根结点的权值最小的树合并,作为一棵新树的左、右子树,且新树的根结点权值为其左、右子树根结点权值之和;
- 从森林中删除选取的两棵树,并将新树加入森林;
- 重复2、3步,直到森林中只剩一棵树为止,该树即为所求得的哈夫曼树zhuanlan.zhihu.com。
构造哈夫曼树的目标就是要使得这个树的带权路径长度最小。为了达到这个目标,在构造过程中,根据贪心算法的原则,每次都选择两个权值最小的节点进行合并,合并后的新节点的权值是这两个节点权值的和zhuanlan.zhihu.com。
哈夫曼树的一个重要应用是哈夫曼编码,它是一种有效的编码方法,用于数据的压缩和解压缩。在哈夫曼编码中,使用频率高的字符编码长度短,使用频率低的字符编码长度长,这样可以最小化整个消息的编码长度zhuanlan.zhihu.com。
什么是权值?
n个权值=n个叶子结点;
pind.com解惑:
在最优二叉树(也称为哈夫曼树)中,权值(权重)是指树中节点所赋予的数值。这个数值通常有特定的含义,比如在哈夫曼编码中,字符的出现频率就是字符的权重。权值越大的节点在哈夫曼树中离根节点越近,这样可以保证权值大的节点的路径长度更短,从而最小化整棵树的带权路径长度
树的带权路径长度-是从树根到每一个叶子之间的路径长度之和.
结点的带权路径长度-为从该结点到树根之间的路径长度与该结点权值的乘积;
树的带权路径长度=树种所有叶子结点的带权路径长度之和 记为WPL;
构造最优二叉树(哈夫曼树)
哈夫曼编码
压缩比
线索二叉树
线索二叉数在存储结构上类型(二叉链式存储)->未存储左子树或右子树的指针域存储它的中序遍历的顺序中的前驱或后继;
任意一个二叉树都可以是线索二叉树
图
图的定义
图中
数据元素用顶点表示;
数据元素之间的关系用边表示;边是两顶点之间的直接关系(不是路径)
有向图和无向图
完全图
无向完全图
n个顶点的无向完全图的边数
有向完全图
n个顶点的有向完全图的边数
顶点的度
顶点的度是指关于该顶点的边数;
有向图中:
指出去的为出度;
指向该顶点的度为入度;
不论是无向图或有向图,总度数与边数的关系;
总度数=2*(边数);
路径
路径长度=路径上边或弧的数目;
连通图与强连通图
连通图->任意两个顶点之间有路径能从一个顶点达到另一个顶点;
无向连通图
n个顶点最少n-1条边构成无向连通图;
最多(等差数列求和):(n-1)*n/2条边;
强连通图
强连通图最少为一圈闭环,即可实现任意两个顶点都是连通的;即n个顶点最少边数为n,构成强连通图;
最多就是除自己以外都和其他顶点有方向连接
边数为n(n-1);
邻接矩阵
邻接链表
稠密图和稀疏图
示例:
空间利用率邻接表,邻接矩阵
图的遍历
深度优先遍历(DFS)
核心:回溯递归;
访问图中所有结点,如果还未访问完(就回溯到上一结点看是否有路子)->直到遍历一遍图中所有结点;类似栈(先进后出)
深度优先的时间复杂度
深度优先的时间复杂度是跟图的存储结构有关;
邻接矩阵nxn遍历完(最多的情况);->深度优先遍历邻接矩阵的时间复杂度o(nxn);
最少的情况是o(n);
邻接链表结点遍历完->遍历所有结点的邻接点o(e)->遍历完所有结点o(n);
所以深度优先遍历邻接链表的时间复杂度为o(n+e);
广度优先搜索遍历(BFS)
广度优先搜索遍历:
先将第一个结点的邻接结点访问完,再访问邻接结点的邻接结点;直到访问完一遍图中所有结点;类似队列(先进先出)
广度优先搜索遍历的时间复杂度
网
拓扑排序
AOV网(有向无环图)
如果出现有向环,意味着活动必须以自身任务的完成为先绝条件,即(出现既是前驱又是后继的情况)->产生矛盾效应;
拓扑排序及其算法(拓扑排序序列)
查找
平均查找长度
顺序查找
折半查找(二分查找)
![image-20231009222710921](https://img-blog.csdnimg.cn/img_convert/c90a4950218c8d1f51429ac44a8ecafb.png)
平均查找长度
哈希表
哈希表的定义
哈希函数构造与冲突处理(线性开放地址法)
冲突处理和装填因子
冲突处理方法(二次探测再散列)
链地址法
哈希表的查找
小顶堆与大顶堆
小顶堆大顶堆的构造
对二叉树(调整为新堆 ),从下往上按照小顶堆或大顶堆的规则交换元素的位置,最后回溯看整体是否满足小顶堆或大顶堆的规则;
排序
基本概念
直接插入排序
从前往后遍历,如果是需要最后是升序;后面数值大于前面数值就不交换,否则就交换位置,交换位置后还需要往前比较,看是否小于前面位置的数值,如果小于重复交换;
示例要求升序(最小比较次数,原序列为升序时)
直接插入排序的时间复杂度
希尔排序
希尔排序的时间复杂度
计数排序
序列里面数值只有1-9的情况;记录个数(序已排好从1-9);
简单选择排序
简单选择排序(不稳定,归位)
简单选择排序的时间复杂度
堆排序
归位,确定位置
堆排序的时间复杂度
冒泡排序
快速排序
第一个位置为a[1]=privot
先从后往前j迭代找小于privot的数值元素,将该元素赋值给i位置
然后从前往后找大于privot的数值元素,将该元素赋值给j位置
最后a[i]=privot;重复以上操作直到(i==j)推出循环;
快速排序的时间复杂度
当原序列位有序时快排序的时间复杂度最坏;(因为快排序第一次为从后往前找最小的,如果原序列为有序就会遍历完整个序列,然后将序列划分为1|2 ,3…,n-1),再遍历后面时又依次递归;
快速排序(取尾),(取中间)
快速排序的平均空间复杂度,最好或最坏的时间复杂度
归并排序
归并排序分组的规则
www.pind.com解惑:
归并排序是一种基于归并操作的有效稳定排序算法,它使用了分治法(Divide and Conquer)的思想。归并排序的基本步骤包括分割、排序和合并[1][8]。
在分割阶段,算法将数组分成两个子数组,通常是平均分割。这个过程会一直进行,直到每个子数组只剩下一个元素为止[1][7]。
然后,算法会递归地对这些子数组进行排序。这个过程就是所谓的“排序”步骤,它使用了同样的归并排序算法来对子数组进行排序[1][8]。
最后,算法将排好序的子数组合并成一个新的有序数组。这个过程涉及到两个指针和额外的空间,它会从两个子数组中取出一个元素,比较它们的大小,然后将较小的元素放入新的数组中。这个过程会一直进行,直到两个子数组都被取完,然后将剩下的元素放入新的数组中[1][2]。
这就是归并排序的基本思想和步骤。归并排序的时间复杂度为O(nlogn),空间复杂度为O(n),它是一个稳定的排序算法,也就是说相等元素的相对顺序在排序后不会改变[3][8]。