完全二叉树:子节点从左往右生长,一层生长完才可以生长一层。
如果树为空,返回false
否则,开始层次遍历二叉树用队列
如果一个节点的左孩子为空,右孩子不为空,则返回false
如果一个结点的左孩子为不空,并且右孩子为空 或者 右孩子和左孩子都为空那么进行下面的判断
首先,树高是树中结点的最大层 数
递归求解高度:
后序遍历:
对每个结点进行如下操作:
//递归
/*递归走到树的最底层,再通过比较左右子树的高度,取较高一棵+1,一直累加到树根。*/
int Height(Tree& t){
if(t == NULL) return 0; //当前结点为空,返回0
//由下至上比较左右子树高度得到最终高度
return Height(t->left) > Height(t->right) ? Height(t->left) + 1 : Height(t->right) + 1;
}
//非递归
/*改编自层次遍历,由根向下每遍历一层,深度+1*/
int Depth(Tree& t) {
if(t == NULL) return 0;
int depth = 0; //树的深度
queue q;
q.push(t);
while(!q.empty()){
int n = q.size();
for(int i = 0;ileft) q.push(s->left);
if(s->right) q.push(s->right);
}
depth += 1; //深度+1
}
return depth;
}
层次遍历法(广度优先遍历)求二叉树高:
使用队列,外层循环使用while循环判断队列q不为空,然后每一次外层循环中都获得队列中元素的个数即这一层次中的宽度n,内层循环使用for循环,即遍历n次,每一次内部循环都取出队列头,将该结点的子结点都入队。在内部循环接收在外部循环内每一次高度加1。当队列中的值都遍历结束后返回高度即树的高度。
顺序查找(哨兵)O(n),
二分查找 O(lgn)
二叉排序树
哈希表
https://blog.csdn.net/qq_56012214/article/details/124430447
**二叉树:**要么二叉树是一棵空树,要么二叉树由根节点,左子树和右子树组成,且左子树和右子树都是二叉树。左子树与右子树有次序。
**满二叉树:**二叉树中的每一层的结点数目都达到了该层的最大结点数目
**完全二叉树:**除了最后一层外,二叉树的每一层节点数都达到当前层的最大节点数目,且最后一层节点数目只有从左到右存在若干节点,这些连续结点的右边不存在结点。
二叉排序树:
二叉树的左子树的所有结点的数据域均小于或等于根节点的数据域,右子树的所有结点的数据域均大于根节点的数据域。
**平衡二叉树:**前提是二叉排序树,任意节点的左子树与右子树高度之差的绝对值不超过1,子树也为平衡二叉树
B树:
一棵m阶B树或者是一棵空树,或者是满足要求的m叉树:
B+树:
红黑树:
满足红黑性质的AVL树:
比较项 | 数组 | 链表 |
---|---|---|
逻辑结构 | 1.数组在内存上连续;2.使用数组之前必须事先固定数组长度,不支持动态改变数组大小;3.数组元素增加时,有可能数组越界;4.数组元素减少时,会噪声内存浪费;5.数组增删时需要移动其他元素 | 1.在内存上不连续;2.采用动态内存分配方式,支持动态增加或者删除元素;3.需要malloc或者new申请内存,不用时需要用free或者delete释放内尺寸 |
内存结构 | 数组从栈上分配内存,使用方便,但是自由度小 | 链表从堆上分配内存,自由度大 |
访问效率 | 数组在内存中顺序存储,可通过下标访问,访问效率高 | 链表访问效率低,访问某个元素需要从头遍历 |
越界问题 | 大小固定,存在越界风险 | 无越界风险 |
分治法将问题划分为互不相交的子问题,递归的求解子问题,再将他们的解组合起来求原问题的解;
动态规划应用于子问题相互重叠的情况;
具备以下两种性质的问题:
Dijkstra
Floyd
for(k=1;k<=n;k++)
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
if(e[i][j]>e[i][k]+e[k][j])e[i][j]=e[i][k]+e[k][j]
Bellman-Ford
(1) 弗洛伊德(Floyd)算法:图中的每一个顶点都是出发顶点,需要求出每一个被看成为出发顶点的顶点到其他顶点的最短路径
(2)迪杰斯特拉(Dijkstra)算法:选定图中某一个顶点作为出发顶点,求出出发顶点到其他顶点的最短路径
稳定 :如果a原本在b前面,而a=b,排序之后a仍然在b的前面;
不稳定 :如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;
内排序 :所有排序操作都在内存中完成;
外排序 :由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
时间复杂度 : 一个算法执行所耗费的时间。
空间复杂度 :运行完一个程序所需内存的大小。
冒泡排序 是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。(把最大的交换到无序区的最后)
稳定排序。
function bubbleSort(arr) {
varlen = arr.length;
for(vari = 0; i < len - 1; i++) {
for(varj = 0; j < len - 1 - i; j++) {
if(arr[j] > arr[j+1]) { // 相邻元素两两对比
vartemp = arr[j+1]; // 元素交换
arr[j+1] = arr[j];
arr[j] = temp;
}
}
}
returnarr;
}
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。(查找最小的放到无序区的最前,保存最小数的索引与第i个交换)
不稳定排序。
function selectionSort(arr) {
varlen = arr.length;
varminIndex, temp;
for(vari = 0; i < len - 1; i++) {
minIndex = i;
for(varj = i + 1; j < len; j++) {
if(arr[j] < arr[minIndex]) { // 寻找最小的数
minIndex = j; // 将最小数的索引保存
}
}
temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
returnarr;
}
通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
function insertionSort(arr) {
varlen = arr.length;
varpreIndex, current;
for(vari = 1; i < len; i++) {
preIndex = i - 1;
current = arr[i];
while(preIndex >= 0 && arr[preIndex] > current) {
arr[preIndex + 1] = arr[preIndex];
preIndex--;
}
arr[preIndex + 1] = current;
}
return arr;
}
希尔排序是把记录按下表的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
不稳定排序。
稳定排序。
步骤1:从数列中挑出一个元素,称为 “基准”(pivot );
步骤2:重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
步骤3:递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
最佳情况:T(n) = O(nlogn)
最差情况:T(n) = O(n2)
平均情况:T(n) = O(nlogn)
优点: 缺点:很脆弱,容易平方级
快速排序是原地排序(只需要非常小的一个辅助栈)
快速排序时间消耗,长度为N的数组排序时间与NlgN成正比
public class QuickSort {
public static void quickSort(int[] arr) {
// 对函数进行封装
quickSor(arr, 0, arr.length - 1);
}
public static void quickSor(int[] arr, int left, int right) {
if (left >= right) return;
int i = left;// 左哨兵
int j = right;// 右哨兵
int index = arr[i];// 基准数
int t = 0;
while (i < j) {
//当右侧数据大于基准数时,右指针向左扫描
while (arr[j] > index) {
j--;
}
//当左侧数据小于基准数时,左指针右左扫描
while (arr[i] < index) {
i++;
}
//当二者都停下时,交换数据
if (i < j) {
t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
//当整个过程完成时,将基准数和分界处的数据互换
arr[left] = arr[i];
arr[i] = index;
//递归处理
quickSor(arr, left, i - 1);
quickSor(arr, i + 1, right);
}
}
}
快排和归并区别:
归并排序的思路是先递归切割,然后依次向上归并,自下而上完成排序;快速排序则是先划分,再递归切割,然后划分,再切割,切割完成排序完成,自上而下完成排序。
归根到底,归并排序和快速排序在递归和关键操作(归并和划分)执行顺序上的不同,是由归并和划分的本质不同造成的。归并的前提是两个有序数组,所以需要一直递归向下切割直到找到两个只有1个元素的基本数组,然后就可以归并了,所以必须先递归切割再归并;而快速排序的向下递归切割,必须依赖划分的结果(划分完成一次排序,然后返回标杆位置),所以必须先划分再递归切割数组。
第8非叶子节点的索引就是arr.length / 2 -1。
// 先根据堆性质,找出它左右节点的索引
int left = 2 * i + 1;
int right = 2 * i + 2;
1、将带排序的序列构造成一个大顶堆,根据大顶堆的性质,当前堆的根节点(堆顶)就是序列中最大的元素;2、将堆顶元素和最后一个元素交换,然后将剩下的节点重新构造成一个大顶堆;3、重复步骤
不稳定。
最佳情况:T(n) = O(nlogn)
最差情况:T(n) = O(nlogn)
平均情况:T(n) = O(nlogn)
基数排序 vs 计数排序 vs 桶排序
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:
计数排序要求输入的数据必须是有确定范围的整数。
计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。
假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排
基数排序也是非比较的排序算法,对每一位进行排序,从最低位开始排序,复杂度为O(kn),为数组长度,k为数组中的数的最大的位数;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-erFU45vy-1664520350175)(山大软院面试.assets/image-20220710083036134.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l5azB6JM-1664520350177)(山大软院面试.assets/image-20220710083109521.png)]
定性描述该算法的时间
优点 | 缺点 | |
---|---|---|
顺序表 | 1. 存储密度大 2.易于查找与修改 | 1.插入与删除元素时不方便2.存储空间利用率低,可能造成存储空间浪费 |
链表 | 1.插入删除元素方便2.存储空间利用率高 | 1.存储密度低 2.查找与修改需要遍历整个链表 |
头指针:
头结点:
首先从头节点开始,依次遍历单链表的每一个节点。每遍历到一个新节点,就重新遍历已经遍历的结点判断该节点是否已经遍历过,如果发现新节点之前的所有结点中存在相同结点ID,则说明结点被遍历过两次,链表有环,否则,就继续遍历下一个新节点。O(n^2) O(1)
首先创建一个以节点ID为键的HashSet集合,用来存储曾经遍历过的节点。然后同样是从头节点开始,依次遍历单链表的每一个节点。每遍历到一个新节点,就用新节点和HashSet集合当中存储的节点作比较,如果发现HashSet当中存在相同节点ID,则说明链表有环,如果HashSet当中不存在相同的节点ID,就把这个新节点ID存入HashSet,之后进入下一节点,继续重复刚才的操作 O(N) O(N)
首先创建两个指针1和2,同时指向这个链表的头节点。然后开始一个大循环,在循环体中,让指针1每次向下移动一个节点,让指针2每次向下移动两个节点,然后比较两个指针指向的节点是否相同。如果相同,则判断出链表有环,如果不同,则继续下一次循环。
O(N) O(1)
new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。
区别:
Ax=λx (A-λE)x=0
A有n个线性无关的特征向量
可数集(Countable set),是指每个元素都能与自然数集N的每个元素之间能建立一一对应的集合。
不可数集,不可数集是无穷集合中的一种。一个无穷集合和自然数集合之间要是不存在一个双射(不存在一一对应关系/法则),那么它就是一个不可数集。
1.Sigmoid
特点 | 缺点 |
---|---|
能够把输入的连续实值变换为0和1之间的输出,特别的,如果是非常大的负数,那么输出就是0;如果是非常大的正数,输出就是1. | 在深度神经网络中梯度反向传递时导致梯度爆炸和梯度消失,其中梯度爆炸发生的概率非常小,而梯度消失发生的概率比较大。 |
2.tanh(x)
3.ReLU函数
1.监督学习
监督学习模型主要可以划分为以下两种:
2.无监督学习
3.强化学习
https://blog.csdn.net/m0_52571748/article/details/119512934
特权指令:只能由操作系统内核部分使用,不允许用户直接使用的指令。
如,进程管理(可软中断)I/O指令、置终端屏蔽jincluding指令、清内存、建存储保护、设置时钟指令(这几种记好,属于内核态)。
非特权指令:
所有程序均可直接使用。
(1)根本区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位。
在开销方面:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
所处环境:在操作系统中能同时运行多个进日4程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)
线程间通信:
线程间通信常用的方法有如下三种:
①全局变量。由于属于同一个进程的各个线程共享操作系统分配该进程的资源,故解决线程间通信最简单的一种方法是使用全局变量。
②参数传递方式。主线程创建子线程并让其子线程为其完成特定的任务,主线程在创建子线程时,可以通过传给线程函数的参数和其通信。
③Message消息机制。常用的Message通信的接口主要有两个:PostMessage和PostThreadMessage。PostMessage为线程向主窗口发送消息,而PostThreadMessage是任意两个线程之间的通信接口。
④线程同步法。还可以通过线程同步来实现线程间通信。例如有两个线程,线程A写入数据,线程B读出线程A准备好的数据并进行一些操作。这种情况下,只有当线程A写好数据后线程B才能读出,只有线程B读出数据后线程A才能继续写入数据,这两个线程之间需要同步进行通信。
轮询与中断是外部设备与中央处理器交互的两种方式。
中断:
由硬件或软件发出IRQ(中断信号),一旦CPU接收到中断信号,CPU暂停当前执行的任务,并保留现场,去相应中断请求。
轮询:
I/O设备有状态寄存器,用来描述设备当前的工作状态,当设备的状态发生改变时,设备将修改相应状态寄存器位。通过不断查询设备的状态寄存器,CPU可以了解设备状态,从而进行必要的I/O操作。
进程(Process
)是资源分配的基本单位,线程(Thread
)是CPU调度的基本单位。
https://www.plju.com/?id=20666
锁机制:包括互斥锁、条件变量、读写锁
wait/notify 等待
Volatile内存共享
信号量机制:
信号机制
https://juejin.cn/post/6969122698563682311
内核在创建进程的时候,会为进程创建相应的堆栈。每个进程会有两个栈,一个是用户栈,存在于用户空间,一个是内核栈,存在于内核空间。当进程在用户空间运行时,cpu堆栈指针寄存器里面的内容是用户堆栈地址,使用用户栈;当进程在内核空间时,cpu 堆栈指针寄存器里面的内容是内核栈空间地址,使用内核栈。
https://blog.csdn.net/weixin_43117620/article/details/109198252
概念:事务 是一个操作序列,包含了一组数据库操作命令。事务把这些命令作为一个整体向系统提交或撤销操作请求,即这些数据库操作命令要么执行,要么不执行,因此事务是一个不可分割的逻辑单元。
特性:
ACID
https://blog.csdn.net/m0_52571748/article/details/119513761
ICMP:Internet Control Message Protocol 网络控制报文协议
网络层有三个协议在IP协议中:
ARP 地址解析协议 Address Resolution Protocol
ICMP 网际控制报文协议 Internet Control Message Protocol
IGMP 网际组管理协议 Internet Group Management Protocol
分为 ICMP差错报文 和 ICMP询问报文
https://blog.csdn.net/raidtest/article/details/40212789
电路交换 报文交换 分组交换
频分复用 时分复用 波分复用 码分复用
网络层是不是可靠传输的?
网络层只检查IP数据报的首部,不检验数据部分是否出错,而传输层对收到的报文进行首部和数据部分进行差错检测。
端与端通信?
应用进程之间的通信,在传输层之间。
逻辑通信的意思?
传输层之间的通信好像是沿水平方向传送数据,但事实上两个传输层之间并没哟一条水平方向的物理连接。
用户单击鼠标后发生事件的顺序:
OSI七层模型:
物理层:利用物理传输介质为数据链路层提供物理连接
数据链路层:定义通过通信媒介互连的设备之间传输的规范
网络层:寻址和路由;
传输层:为上层协议提供端到端的可靠传输;
会话层:建立、断开和维护通信连接;
表示层:数据格式转换、数据压缩和数据加密;
应用层:为应用程序提供网络服务;
TCP五层模型:相比OSI七层模型,会话层,表示层,应用层合为应用层。
应用层:为应用程序提供服务
联系:
协议,是控制对等实体间进行通信的规则集
协议的实现保证了能够向上一层提供服务。要实现本层协议,还需要使用下面一层所提供的服务.
区别:
“水平的”
,即协议是控制两个对等实体进行通信的规则。“垂直的”
,即是下层
通过层间接口向上层
提供服务。(1)面向连接的服务:是指通信前需要建立连接,通信结束后需要进行连接释放。整个过程包括,连接建立、数据传送、连接释放。是按序传送、可靠传送的。 此外还包括传输可靠性的各种措施,比如超时重传、流量控制等;常见面向连接传输TCP。(2)无连接的服务:传送数据之前不许要建立连接,随时传就行,但是无法避免数据的丢失、重复等只能“尽最大努力地交付”,是不可靠地传输,常见面向无连接传输有UDP
都是传输层协议,是用来建立端到端的可靠传输的。
UDP仅提供最基本的数据传输功能,而不关心传输时的连接建立和断开、传输可靠性的保证,而是把这些问题都抛给了UDP上层的应用层程序处理。
TCP是一种面向连接的协议,包括传输时连接建立,数据传送,连接释放,还包括保证可靠传输的措施,如超时重传、流量控制。
区别如下:
TCP | UDP |
---|---|
有连接 | 无连接 |
每条TCP连接只能一对一,全双工通信 | 支持一对一、一对多、多对一、多对多交互通信 |
面向字节流,即把应用层传来的报文看成字节流,将字节流拆分成大小不等的数据块,并添加TCP首部; | 面向报文,对于应用程序传下来的报文不合并也不拆分,只是添加UDP首部,首部开销小 |
可靠交付,TCP支持传输可靠性的多种措施,如流量控制,拥塞控制 | 尽最大努力交付,仅提供最基本的数据传输能力。 |
TCP对应典型的应用层协议:
UDP对应典型的应用层协议:
先听后发、边听边发、冲突停发、随机重发。
routing information protocol 路由信息协议
一种分布式的基于距离向量的路由选择协议
RIP协议要求网络中每一个路由器都维护从它自己到其他每一个墓地网络的唯一最佳距离记录
RIP的缺点:
特点