链表与数组
数组,在内存上是连续的空间;链表,在内存上可以不连续,每个链表节点包括数据可其他节点的地址信息。
数组优点:
- 内存空间占有少,因为链表节点保存其他节点的信息。
- 数组内的数据是随机访问的,数据查找速度快
链表优点:
- 数组需要连续内存空间,在数组过大时可能引起内存不足的问题,链表则不需要大块连续内存
- 插入删除操作方便,不需要移动数据
- 数组一旦创建便不能改变容量,而链表可以
堆、栈(堆栈)、队列
堆
- 堆通常是一个可以被看做一棵树的数组对象。堆的特点是父子节点有序、完全二叉树。
- 父节点总大于其子节点的堆被称为大根堆,反之为小根堆。
- 堆在程序运行时生成,而不是编译时生成,是动态进行内存分配的。是程序中申请的动态内存的特殊数据结构
- 程序员申请释放,系统也可以释放
- 二级缓存中,JVM垃圾回收释放
栈
- 又名堆栈,是一种运算受限的线性表。仅允许在一端进行插入删除运算,通常称为栈顶,另一端称为栈底。
- 先进后出原则
- 编译时指定栈的大小,之后不可更改。
- 是操作系统建立线程时建立的存储区域,具有FILO的特性。由操作系统自动分配释放。
- 一级缓存,用完即释放
- PUSH(),POP()
队列
- 特殊的线性表,只允许前端删除,后端插入。分布被称为队头队尾。
- 没有元素时,称为空队列
- 队列可以使用数组和链表的方式实现,需要头指针和尾指针进行辅助使用,分别指向出队元素和入队元素。
- FIFO原则
链表的删除,插入,反向
链表的删除
- 遍历链表寻找待删除数据节点cur的前置节点pre
- 删除操作就是将前置节点的下一个节点地址指向待删除的下一个节点。
pre.next=cur.next
- 删除时注意头节点问题
链表的插入
- 遍历链表寻找待插入数据节点cur的前置节点pre
cur.next=pre.next; pre.next=cur
- 注意头节点问题
链表的反向
链表逆序输出的方法:
- 遍历链表将数据记录在数组中,数组反向遍历
- 使用栈逆序输出
- 逆序链表在输出
重点说明第三中方法
Node reverseList(Node head){
//如果链表为空或只有一个元素直接返回
if(head.getNext()==null||head.getNext().getNext()==null){
return head;
}
Node p = head.getNext();
Node q = head.getNext().getNext();
Node t = null;
while(q!=null){
t = q.getNext();
q.setNext(p);
p = q;
q = t;
}
//设置链表尾
head.getNext().setNext(null);
//修改链表头
head.setNext(p);
return head;
}
字符串的操作函数
- 字符串查找:indexOf/lastIndexOf
- 获取索引位置的字符:charAt
- 获取子字符串:substring
- 去除空格:trim
- 字符串替换:replace
- 判断字符串开头结尾:startsWith/endsWith
- 字符串是否相等:equals/equalsIgnoreCase
- 按字典须比较字符串:compareTo
- 大小写转换:toLowerCase/toUpperCase
- 字符串分割:spilt
Hash冲突的解决方法
- 开放地址法:使用增量di(线性增加,平方再探测)进行二次散列
- 链地址法:所有hash地址相同的记录都连接在一个链表中
- 再哈希法:改变HASH函数
- 建立公共溢出区:冲突的记录都放在一个公共区域中。
排序概述
冒泡
前后两数进行比较,较大的向后排,每趟排序可以选出一个最大值,形成顺序,反之,称为逆序。
平均时间复杂度,O(n^2)
最坏时间复杂度,O(n^2)
是稳定排序
直接选择排序
每次从待排序数据中选出最小/最大的一个元素,存放在序列的起始位置,直至全部待排序数据元素排完。
平均时间复杂度,O(n^2)
最坏时间复杂度,O(n^2)
不稳定排序
直接插入排序
每次将待排序的数插入已排序的数组中,使其仍然保持有序,直至最后一个完成。
平均时间复杂度,O(n^2)
最坏时间复杂度,O(n^2)
希尔排序
其实质是分组插入排序,又称为做小增量排序。
将整个待排序元组按固定“增量”间隔分割成若干个子序列。对子序列进行直接插入排序,再依次缩减增量,直至整个序列基本有序。再对全体元素进行一次直接插入排序。
平均时间复杂度,O(n^1.3)左右
最坏时间复杂度,O(n^2)
不稳定排序
归并排序
采用分治法的一个典型的应用。
将待排序序列R[0…n-1]看作n个长度为1的有序序列,再将相邻的有序表成对归并,得到N/2个长度为2的有序序列,如此反复,最终得到一个长度为N的有序序列。(折半划分,两两合并排序)
归并排序的形式就是一棵二叉树,遍历的次数是二叉树的深度,则其时间复杂度为O(nlog2n)
快速排序
基于分治法的策略,是一种划分交换排序。
- 先出数列中选取一个数作为基准数
- 分区,大于该数的置于右边,小于或等于该数的置于左边
- 对左右区间分别重复之前的步骤,直至个区间只剩一个数字。
挖坑填数+分治法
平均时间复杂度,O(nlog2n)
最坏时间复杂度,O(n^2)
不稳定排序
堆排序
二叉堆是完全二叉树或近似是完全二叉树。
一般都用数组来表示堆,I节点的父节点下标为(i-1)/2.他的左右子节点分别为2*I+1和2*I+2;
利用大根堆或小根堆的根记录是最大关键字这一特性,使得每次从无序中选择最大记录变得简单。
大根堆的基本思想:
- 将无序序列建成一个大根堆
- 将堆顶元素和无序区的最后一个记录交换,将新的无序区调整为新的堆
- 重复上述过程,直到所有数据都输出过
平均时间复杂度O(nlog2n)
不稳定排序
桶排序和基数排序
桶排序是将数组分到有限数量的桶中,每个桶再个别进行排序(可以是其他排序算法也可以是递归桶排序)
对N个待排数据,M个桶,平均每个桶N/M个数据的桶排序平均时间复杂度:O(N+NlogN-N*logM)
空间复杂度为:O(N+M)
基数排序是按照待排序数据中每组的关键字进行桶排序。性能比桶排序略差。
待排序数据N可以分为d个关键字,则基数排序的时间复杂度需要O(d*2N),当d远远小于N时,基本还是线性级别的,其空间复杂度为O(N+M)
是稳定排序
快排中partition函数和merge函数的实现
public static int partition(int[] a,int low,int high) {
int privotKey = a[low];
while(low<high){
while(low<high&&a[high]>=privotKey)--high;
swap(a,low,high);
while(low<high&&a[low]<privoteKey)++low;
swap(a,low,high);
}
return low;
}
public static int[] merge(int[] a,int low,int high) {
if(low<high){
int privotLoc = partition(a,low,high);
merge(a,low,privotLoc);
merge(a,privotLoc+1,high);
}
}
冒泡和快排的改进
冒泡改进
- 标志变量:用于记录最后一次进行交换的位置,该标志变量之后的记录均已为最后结果,不必进行比较。
- 每次循环正向和反向两次比较,得到最大与最小值,减少趟数。
快排改进
- 数据重复较多时,采取三路划分法,将数组划分成三个部分,大于等于和小于。
- 快排至数据量较小时,转化成插入排序。
- 取中位数作为基准数据
二分查找与变种二分查找
二分查找也称为折半查找法。其基本思想为:在有序表中,取中间记录作为比较关键字,若相等则查找成功,若小于则在左半区查找,若大于则右半区查找。不断重复上述过程,直到查找成功。
- 插值查找法
其改进主要体现在计算mid的公式上:
mid = low + \frac {key - a[low]}{a[high] - key}(high - low)
由于大体框架与二分查找算法是一致的,所以时间复杂度仍然是O(logn)
- 斐波那契查找算法
利用黄金分割比例原理实现。如果一个数列满足F(n)=F(n-1)+F(n-2)则这个数列就是斐波那契数列。在这种数列中的mid计算公式为:
mid = low + F(k-1) - 1
各类树
二叉树、B+树、AVL树、红黑数、哈夫曼树
二叉树
二叉树是每个节点最多有两个子树的树结构,常被用于实现二叉查找树和二叉堆。
- 二叉树每个节点最多有两颗子树,有左右次序。
- 二叉树第I层最多有节点2^{i-1}个
- 深度为K的二叉树之多有2^k-1个节点
- 对任意一颗二叉树,若其叶子结点为n0,度为2的节点为n2,则n0=n2+1;
- 深度为k,且有2^k-1个节点的树为满二叉树
- 有n个节点的二叉树,当且仅当其每一个节点的序号都与满二叉树节点对应时,称为完全二叉树。
遍历顺序:前序、中序、后序、层次、线索二叉树
B+树
B+树是一种树数据结构,为n叉排序树。一棵B+树通常包含根节点、内部节点、叶子结点。
B+树常用于数据库和操作系统的文件系统中,均使用该数据结构作为元数据索引。B+树的特点是保持数据稳定有序,其插入与修改拥有较为稳定的对数时间复杂度。B+数的插入是自底向上插入的。
B+树的定义:B+树是B-树的变形树,其差异为:
- 有n颗子树的节点含有n个关键字,每个关键字不保存数据,只用来存储索引。所有数据都保存在叶子节点中。
- 所有叶子节点中包含了全部关键字的信息,以及包含这些关键字记录的指针,且叶子节点本身以关键字次序自小而大排序的。
- 所有的非叶子结点可以看作索引比分,节点中仅含其子树中最大(或最小)的关键字。
- 通常B+树上有两个头指针。一个指向根节点,一个指向关键字最小的叶子节点。
B+树与B树的区别
- B树叶子节点没有包括全部需要查找的信息
- B树的非叶子节点也包含需要查找的有效信息
B+树的优点
- 磁盘读写代价低:非叶子节点数据量小,读取更快。
- 查询效率稳定:每次查询都是根节点到叶子节点的路径,所有关键字查询路径长度相同。
AVL树
AVL树本身是一颗二叉查找树,任何节点的两个子树的高度最大差别为一。查找、插入、删除在平均和最坏情况下都是O(logn).插入和删除可能需要通过一次或多次旋转平衡这个树。
AVL本质就是一棵具有平衡功能的二叉查找树。
红黑树
红黑书是一种自平衡二叉查找树。红黑树与AVL树类似,都是在插入和删除操作时通过旋转操作保持二叉查找树平衡。可以在O(logn)时间内完成插入和删除,N是树中元素的数目。
性质:红给叔每个节点都带有颜色属性(红色或黑色)
- 节点是红色或黑色
- 根节点是黑色
- 每个红色节点的两个子节点都是黑色(叶子到根的所有路径不能有两个连续的红色节点)
- 从任一节点到其他叶子的所有路径都含有相同数目的黑色节点。
以上性质使得:从根到叶子的最长的可能路径不多于最短的可能路径的两倍。
哈夫曼树
给定n个权值作为n个叶子节点,构造一颗二叉树,若带全路径长度达到最小,称这样的二叉树为最优二叉树,也叫哈夫曼树。
哈夫曼树是带权路径长度最短的树,权值较大的节点离根较近。
基本术语:
- 路径和路径长度:从一个节点到其后代节点之间的通路,称为路径。若根节点层数为1,则根到L层节点的路径长度为L-1.
- 节点的权和带权路径:节点中某种含义的数值。根节点到该节点的路径长度与该节点权的乘积为带权路径。
- 树的带权路径长度:所有叶子结点的带权路径长度之和,WPL。
构造:
- 将n个节点的权值看作n颗树的森林。
- 选出权值最小的两个树合并,生成一棵新树,其权值为左右节点之和。
- 删除选中的树,将新树加入森林。
- 重复以上过程直至剩下一棵树。
图的BFS和DFS算法
BFS算法
广度优先搜索算法,属于盲目搜索算法。类似树的层次遍历。
使用队列保存未被检测的邻居节点,按照广度优先的次序进行搜索。
初始化队列Q;
Q={起点s};标记S已被访问;
while(Q非空){
取Q队首元素u;
u出队;
if(u==目标){...}
所有与u相邻且未被访问的节点入队;
标记u已访问;
}
BFS算法描述:
- 将起点标记为已访问并放入队列。
- 从队列中取出一个顶点,得到与该顶点相通的所有顶点。
- 遍历这些顶点,先判断顶点是否已被访问过,如果否,标记该点为已访问,记录当前路径,并将当前顶点入列。
- 重复2、3,直到队列为空。
DFS算法
深度优先算法,类似树的先根遍历
DFS(dep,...){
if(找到||走不下去){
...
return;
}
枚举下一个节点,DFS(dep+1,...);
最小生成树prim算法与最短路径Dijkstra算法
prim算法
最小生成树
算法简单描述
- 输入:一个加权连通图,其中顶点集合为V,边集合为E;
- 初始化:Vnew = {x}(已选),其中x为集合V中的任一节点(起始点),Enew = {},为空;
- 重复下列操作,直到Vnew = V(两边集合相等):a.在结合E中选取权值最小的边(u,v),其中u为集合Vnew中的元素,v不在其中。b.将v加入Vnew中,将其边加入Enew中。
- 输出:使用集合Vnew和Enew来描述所得到的最小生成树。
Dijkstra算法
基于广度遍历的贪心算法,用于计算一个节点到其他所有节点的最短路径。主要特点是以起始点为中心向外层层层扩展,每次查找与该点路径最小的点,直到扩展到所有节点为止。
引入辅助变量D,每个分量D[i]表示从原点到其他节点的路径长度,如果不能直接到达,则设为无限大。遍历过程中不断更新辅助变量。
算法的简单流程描述。
- 选取一点v[0]作为起始点,初始化辅助遍历D,即v[0]到其他节点的距离。
- v[0]标记为已遍历。v[0]=1;
- 根据辅助变量D,寻找与v[0]路径最短的点v[k],并将v[0]与v[k]的距离记为min。
- v[k]标记为已遍历。
- 查询病比较d[j]与min+w[k][j],将d[j]更新为较小的值,即为最短路径
- 重复3-5过程,直到找出所有的点。
KMP算法
KMP算法是一种改进的字符串匹配算法。该算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。时间复杂度O(m+n)。
算法简单描述
首先,需要生成一张部分匹配表,该表按照字符串“前缀”(除最后一个字符以外的全部头部组合)和“后缀”的共有元素的长度生成。
获取前n位字符串,生成其前后缀,得到共有元素,计算其共有元素长度,将其设置为最后一位字符
的匹配长度。(n:1….n)
- 字符串与模式串第一位比较,直到相同为止。
- 按位与模式串匹配,若相同则匹配结束。若不相同,则获取其最后一位相同的字符和已匹配的字符数。
- 根据字符查询部分匹配表,获取相应的数值,根据
移动位数 = 已匹配的字符数 - 对应部分的匹配值
计算出将搜索词向后移动的位数。 - 重复1,2,3步骤。
- 若要继续匹配,则继续按照之前的公式计算移动位置。
动态规划
其基本思想是将求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解,适合用于动态规划求解的问题,分解得到的子问题往往不是相互独立的。(以空间换时间)
通过保存已解决的子问题的答案,再需要时重新取出,避免大量的重复计算,节约时间。
应用场景:最优化原理、无后效性、子问题的重叠性。(Floyd算法)
简单流程:
- 分析最优解性质,并可话其结构特征
- 递归定义最优解
- 备忘录法记录计算过的数据
- 根据计算最优值的方式构造问题的最优解
贪心算法
在对问题求解时,总是做出当前情况下的最优解,不从整体上考虑。
选择的贪心策略必需具备无后效性。
分治算法
其基本思想是将一个规模为N的问题分解成K个规模较小的问题,这些子问题相互独立且与原问题性质相同,求出子问题的解就等于得到原问题的解。