数据结构01
数据的结构,不仅要有数据,关键是数据它以什么样的形式组织到一起,各种数据之间的关系是什么,这叫数据结构
2.为什么要学习数据结构
①.它是我们编程的指导
②.提升编程的水平
③.面试中经常被问到
3.有哪些数据结构
学习到什么样的程度
①.听懂理论,听懂算法思路
②.完成主要数据结构算法的实现
③.融会贯通,举一反三
数据结构=逻辑结构+存储结构
逻辑层面
线性结构--------例如排队,冰糖葫芦
非线性结构-------例如地图,公司组织架构
存储层面
顺序结构-------数组,链表
特性:
①.类型相同
②有限性
区别:
数组:查询快,插入删除慢
链表:查询慢,插入删除快
Vector和ArrayList有什么区别?
Vector线程安全,效率低
ArrayList线程不安全,效率高
问:Vector的扩容机制是什么?
Vector底层是数组
如果调用无参构造方法创建对象,则初始容量为10,每次以翻倍的方式扩容。
如果调用一个参数的构造方法创建对象,初始容量就是该参数的值,每次以翻倍的方式扩容。
如果调用两个参数的构造方法创建对象,第一个参指定了初始容量,第二个参指定了扩容容量。
链表:这是通过指针串接起来的一系列数据的集合,它的每个小格子可以分两个部分,一部分用来存数据,另一部分用来存下一个小格子的内存地址
注意:在链表代码实现的过程中,我们往往会增加一个头结点,它不存数据,只存第一个元素的内存地址,这样一来,我们对第一个元素的操作,和其他元素的操作都是一样,简化了我们的编程,否则得分两种情况考虑
双向链表
每一个节点除了自己本身的数据之外,还分别有两个指针,一个用来指向它的后继节点,一个用来指向它的前驱节点
如果首尾相连,称为循环双向链表
java中的LinkedList是双向链表
该LinkedList的add方法操作内部变化如图:
栈-------堆栈,它是运算受限的线性表,我们只能在栈的一端进行插入删除操作
栈顶----进行插入删除操作的那一端
栈底----插入删除操作错误的另一端
栈的特点:先进后出,后进先出
对于栈来讲,有三个操作:
push ---入栈
pop -----出栈
peek ----获取栈顶元素
队列queue----它和栈一样,也是一种运算受限的线性表只允许在一端进行插值,从另外一端进行取值插入数据的这段称为队尾
删除数据的这端称为队首
队列的操作:
enqueue ----入队
dequeue ----出队
peek ---------获取队首元素
栈和队列,他们的存储结构既可以是数组,也可以采用链表
Java类
Stack ----栈(过时)
Queue 接口--- 队列
Deque 接口 ----双端队列
注意:在Java中,实现栈和队列,往往都是通过使用LinkedList类实现,所以底层是链表
树和二叉树
树:由根节点和其子节点构成的数据结构
节点的度:该节点拥有的子树的数目称为节点的度
树的度:各个节点的度的最大值
注意:如果一个节点,度是0,称为叶子节点或终端节点
度不为0的节点称为非终端节点,或者分支节点
节点的层次:从根开始定义,根的层次是1,其子节点层次是2...
树的深度:树中节点最大的层次数称为树的深度
有序树:
如果树中的节点的各个子树看成是从左至右是有次序的,则称为有序树,如果不考虑顺序,则是无序树
m叉树:一棵树的任一节点往下最多分几个叉,就是几叉树
深林:m棵互不相交的树的集合 m>=0
二叉树:每个节点的度均不超过2的有序树
二叉树可以递归定义
二叉树的每个节点的孩子数只能是0或者1或者2
并且每个孩子都有左右之分
满二叉树:每个层的节点都达到最大数
第一层:1个
第二层:2个
第三层:4个
第四层:8个
....
完全二叉树:
在满二叉树上,最下层,从右侧起,去掉若干相邻的子节点,得到的二叉树
如果将完全二叉树的最下面一层去掉,那又变成满二叉树,满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树
二叉树的性质:
①对于任何一个二叉树,如果其叶子节点数为n0,度为2的节点数为n2,那么n0=n2+1
②在二叉树的第i层上最多有2的i-1次方个节点
二叉树的存储结构
数组,链表
往往都是用链表(二叉链表)
在二叉链表中,每个节点包含四个小格子
其中一个格子用来存放数据
剩下的三个格子分别用来存放它的左孩子,右孩子,父节点的地址
二叉树的遍历方式:
先序遍历:根,左子树,右子树
中序遍历:左子树,根,右子树
后序遍历:左子树,右子树,根
注意:在遍历的过程中,不管是哪棵子树,都是相同的遍历思想,所以往往我们会采用递归算法进行遍历
注意:给出中序,然后先序后序随便给一个,求另一个,一定能求出来
面试题:
有一个二叉树,它的后续遍历为:
5 4 3 7 6 2 1
它的中序遍历为:
4 5 1 3 2 6 7
请问,它的先序遍历是什么?
思路:
①根据后序可知最后一个树是根,所以1是根
所以先序:1 ....
②根据中序可知 4 和 5是1的左子树,3267是1的右子树
③由后序可知54的顺序决定了4是左子树的根
所以先序:1 4 5 ....
④在中序中4在前,5在后,所以5是4的右孩子
⑤由后序可知,2肯定是右子树的根
所以先序:1 4 5 2....
⑥由中序可知3是2的左孩子,67是2的右孩子
所以先序:1 4 5 2 3....
⑦由后序可知 6是7的父节点,所以6在前7在后
所以先序:1 4 5 2 3 6 7....
⑧看中序67的顺序可知7是6的右孩子
递归---程序调用自身的一种编程技巧,它能将一个大型的复杂问题层层转化为一个与原问题相似的较小的问题来求解,减少了程序代码量。
判断算法的优劣
①事后统计法
②事前估算法
时间频度:一个算法中语句的执行次数称为时间频度T(n)
int sum=;
for(int i =1;i<=100;i++){
sum+=i;
}
一共执行了303条语句
高斯算法
(1+100)*100/2=5050
时间频度中的注意事项:
①忽略常数项
随着算法规模越来越大,常数项在整个时间频度中的差距将越来越无关紧要
T(n)=2 * n 和 T(n)=2*n +10
n=1 2 12
n=100 200 210
n=30000 60000 60010
②忽略常数项
随着算法规模越来越大,低次幂在整个时间频度中的差距将越来越无关紧要
例: T(n)=2*n^2+3n+10 和 T(n)=2 * n^2
n=1 15 2
n=100 20310 20000
n=30000 1800090010 180000000
③最高次幂的系数可以忽略
算法的时间复杂度
算法的时间复杂度可以看做它的时间频度T(n)去掉常数项,去掉低次幂,去掉最高次幂,剩下的部分,记做O(n)
例:
T(n)=3*n^2+7 * n + 6
那么该算法的时间复杂度记做O(n^2)
常见的时间复杂度:
①常数阶O(1)
int i=1;
int j=2;
++i;
j++;
②对数阶O(log a n)
如果n=a^x,那么x就等于 以a为底n的对数,记做log a n
例如:
int i=1;
while(i<n){
i=i*2;
}
③线性阶O(n)
int i=1;
while(i<n){
i=i++;
}
④线性对数阶O(nlog 底数N)
for(int m=1;m<n;m++){
int i=1;
while(i<n){
i=i*2;
}
}
⑤平方阶O(n^2)
for(int x=1;x<=n;x++){
for(int i=1;i<=n;i++){
j=i;
}
}
⑥立方阶O(n^3)
参考平方阶,三层for循环
⑦k次方阶
⑧指数阶O(2^n)
注意:一般我们讲时间复杂度,不是平均时间复杂度,而是最坏时间复杂度。
也就是在当前的业务逻辑中n值所能取到的最大规模
各个排序算法的时间复杂度:
冒泡,选择,插入 他们都是平方阶O(n^2)
归并排序和堆排序属于线性对数阶O(nlog底数N)
希尔排序是一个介于线性阶和平方阶之间的时间复杂度
快速排序是一个介于线性对数阶和平方阶之间的时间复杂度
基数排序是对数阶O(log a n)
冒泡排序:相邻的两个数进行比较,如果逆序,就两两交换,第一轮结束,最大的数排到了最后,第二轮结束第二大的数排到倒数第二的位置...
原数组: 3 9 -1 10 20
第一轮: 3 -1 9 10 20
第二轮:-1 3 -2 10 20
第三轮:-1 -2 3 10 20
第四轮:-2 -1 3 10 20
注意:如果有n个数,则需要n-1轮
选择排序:
第一次从arr[0]到arr[n-1]中选取最小值,与arr[0]交换
第二次从arr[1]到arr[n-1]中选取最小值,与arr[1]交换
依次类推.....
总共通过n-1轮比较,就得到了一个从小到大的有序序列
例:
原始数组: 101 34 119 1
第一轮: 1 34 119 101
第二轮: 1 34 119 101
第三轮:1 34 101 119
插入排序:
我们将原数组看做一个无序数组
然后,再新创建一个有序数组
不停的从无序数组中取出值,来插入到有序数组中的指定位置
例:
原数组:17,3,25,14,20,9
初始状态:
有序数组:17 无序数组:3,25,14,20,9
第一次插入:
有序数组:3,17 无序数组:25,14,20,9
第二次插入:
有序数组:3,17,25 无序数组:14,20,9
第三次插入:
有序数组:3,14,17,25 无序数组:20,9
第四次插入:
有序数组:3,14,17,20,25 无序数组:9
第五次插入:
有序数组:3,9,14,17,20,25 无序数组:
希尔排序
arr={2,3,4,5,6,1}
arr={2,3,4,5,6,6}
arr={2,3,4,5,5,6}
arr={2,3,4,4,5,6}
arr={2,3,3,4,5,6}
arr={2,2,3,4,5,6}
arr={1,2,3,4,5,6}
当需要插入的数是比较小的时候,并且已经执行到了最后几轮,那么后移的次数将明显增多,影响效率
思路:针对数组按照一定的增量进行分组,对每一组使用插入排序,随着增量的减少,每组包含的元素个数将越来越多,当增量减至1时,整个数组恰好被分成一组,算法结束
例:
arr={8,9,1,7,2,3,5,4,6,0}
第一轮:数组长度length=10,length/2=5,所以我分5组,每组2个数,按照步长为5进行划分
{8,3}{9,5}{1,4}{7,6}{2,0}
然后对每一组分别在原位置上进行插入排序
原数组:
8,9,1,7,2,3,5,4,6,0
第一轮结果:
3,5,1,6,0,8,9,4,7,2
第二轮:5/2=2 所以将第一轮的结果分2组,步长为2,每组5个数
{3,1,0,9,7}{5,6,8,4,2}
然后对每一组分别在原位置上进行插入排序
原位置:
3,5,1,6,0,8,9,4,7,2
第二轮结果:
0,2,1,4,3,5,7,6,9,8
第三轮:2/2=1 所以将第二轮的结果分1组,步长为1,每组10个数
0,1,2,3,4,5,6,7,8,9
在不断分组再对各组插入排序的过程中,其实是逐渐将较小的数集中在相对靠前的位置,较大的数都集中在相对靠后的位置,以此来规避之前发现的性能问题
快速排序
将要排序的数组分成左右两部分,其中一部分的所有数据都要比另外一部分所有数据小
然后再按此方法对这两部分数据分别进行快速排序
例:
arr={-9,78,0,2,3,-567,70,-1,-6}
首先使用数组长度除以2,得到中间那个数
9/2=4
arr[4]是3
3作为基准
第一轮
{-9,6,0,2,-1,-567,3,70,78}
将数组分成了
{-9,6,0,2,-1,-567}3{70,78}
{-9,-6,0,-567,-1},2,3,70,78
-567,-9,-6,-1,0,2,3,70,78
归并排序
采用经典的分治策略,将问题分成一些小问题,再递归求解,首先拆分,然后将分的阶段得到的答案修补在一起
例:
原数组{8,4,5,7,1,3,6,2}
第一次拆分:8,4,5,7和1,3,6,2
第二次拆分: 8,4和5,7和1,3和6,2
第三次拆分:8和4和5和7和1和3和6和2
下面开始合并
第一次合并(各自排序):4,8和5,7和1,3和2,6
第二次合并(各自排序):4,5,7,8和1,2,3,6
第三次合并(各自排序):1,2,3,4,5,6,7,8
合并的推导过程:
首先创建一个临时数组
再创建i和j两个变量分别指向两数组的开头4和1
LinkedList
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)