这份教程针对数据结构与算法的基础重点进行梳理,以便快速复习相关知识点以及思想,不求全,而求精。
一、二叉树
栈和队列就不说了,栈是先进后出,队列是先进先出。树也有几个专业名词,分别是:高度(从下往上)、深度(从上往下)、层、双亲节点、叶子节点、度(节点分支的个数)。其中树的度指的是各个节点中度的最大值。
一般二叉树的节点声明如下:
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
}
满二叉树和完全二叉树,其第i个节点的孩子节点为2i和2i+1,双亲节点为i/2。
1、二叉树的遍历
二叉树的遍历是经典的递归算法,分别有前序遍历、中序遍历以及后序遍历。
(1)前序遍历
public void preOrder(TreeNode root){
System.out.println(root.val);
preOrder(root.left);
preOrder(root.right);
}
(2)中序遍历
public void inOrder(TreeNode root){
inOrder(root.left);
System.out.println(root.val);
inOrder(root.right);
}
(3)后序遍历
public void postOrder(TreeNode root){
postOrder(root.left);
postOrder(root.right);
System.out.println(root.val);
}
(4)BFS-层序遍历-广度优先搜索
BFS【层序遍历】需要使用队列进行辅助:
- 队列先添加根节点
- 如果队列不为空则执行poll操作
- 记录每层的节点数(队列的大小),每次都将同一层的节点poll出
- 每次poll一个节点,将其存储的值记录,并将其孩子节点加入队列
class Solution {
public List<List<Integer>> BFS(TreeNode root) {
List<List<Integer>> list = new LinkedList<>();
if(root == null){
return list;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
while(queue.size()>0){
int levelLen = queue.size();
List<Integer> valList = new LinkedList<>();
for(int i=0;i<levelLen;i++){
TreeNode temp = queue.poll();
valList.add(temp.val);
if(temp.left != null){
queue.add(temp.left);
}
if(temp.right != null){
queue.add(temp.right);
}
}
list.add(valList);
}
return list;
}
}
(5)DFS-深度优先搜索
与BFS不同,深度优先是利用栈作为辅助:
- 初始化栈,将根节点加入到栈中
- 从栈中pop出节点,并将该节点的右节点和左节点加入到栈中:一定是先右后左,才能先左再右的pop
class Solution {
public List<Integer> BFS(TreeNode root) {
List<Integer> list = new LinkedList<>();
if(root == null){
return list;
}
Stack<TreeNode> stack = new Stack<>();
stack.add(root);
while(stack.size()>0){
TreeNode t = stack.pop();
list.add(t.val);
if(t.right != null){
stack.push(t.rigth);
}
if(t.left != null){
stack.push(t.left);
}
}
return list;
}
}
2、二叉查找树
二叉查找树是一棵左子树小,右子树大的节点。
(1)二叉查找树的判断
判断一棵二叉树是否是查找树,要使用区间判断不能直接进行节点间的判断。
class Solution {
public boolean isValidBST(TreeNode root) {
return isValidBST(root,Long.MIN_VALUE,Long.MAX_VALUE);
}
public boolean isValidBST(TreeNode root,long min,long max){
if(root == null){
return true;
}
if(root.val>=max || root.val<=min){
return false;
}
return isValidBST(root.left,min,root.val) && isValidBST(root.right,root.val,max);
}
}
(2)二叉查找树的删除
二叉查找树的删除分三种情况:
- 删除的节点为叶子节点:直接删除
- 删除的节点有一个子节点:子节点代替
- 删除的节点有两个(以上)子节点:右子树最小(左)节点替换该节点
3、整点B树
(1)平衡二叉树(AVL)
对于每个子树,左右子树的高度差不超过1。通过左旋和右旋来进行树的结构调整。
(2)B树
B树是同一节点有多个元素的树,如4阶B树为2-3-4树,实现较为困难。
(3)B+树
MySQL是底层是以页作为基本存储单元,在页中的数据以单链表+槽(Slot,页目录)的方式存储,而页头会存放Page_Pre和Page_Next指向前后页,从这里便开始采用B+树的数据结构,建立页(双向链表)的索引,即非叶子节点。非叶子节点存储的都是索引列和对应页号,叶子节点存储完整数据。
(4)红黑树
从JDK1.8开始,HashMap的底层数据结构是由“数组+链表+红黑树组成”:
- 当同一索引的节点数量达到9个且数组长度大于64,则触发链表转红黑树
- 当同一索引的节点数量少于6个后,红黑树转为链表
红黑树是通过变色+左右旋操作使树达到平衡,其根节点和叶子节点为黑色空节点,同一简单路径上相邻元素不能同为红色,且黑色节点数相同,插入的节点为红色。
二、堆
堆是特殊的完全二叉树,所以一般使用数组来存储,有大顶堆和小顶堆。掌握堆数据结构算法的关键是堆化操作。
以下是大顶堆的堆化操作:
public void buildHeap(int[] arr,int length){
for(int i=length/2;i>=0;i++){
sink(arr,i,length);
}
}
private void sink(int[] arr,int index,int length){
//拿到孩子节点
int leftChild = 2*index+1;
int rightChild = 2*index+2;
//保留双亲节点的位置
int parent = index;
//下沉
if(leftChild<length && arr[leftChild]>arr[parent]) parent = leftChild;
if(rightChild<length && arr[rightChild]>arr[parent]) parent = rightChild;
if(parent != index){
int temp = arr[index];
arr[index] = arr[parent];
arr[parent] = temp;
//递归执行
sink(arr,parent,length);
}
}
三、图
图的表示方法主要为两种:邻接矩阵和邻接表。
邻接矩阵通过二维数组实现,A[i][j]代表i点和j点的边,其值代表权重。这种存储方式的弊端是不利于存储稀疏图,会浪费较多的存储空间。(例若用图来表示好友关系,微信用户好几亿,但每个用户的好友只有几百个)。
邻接表类似哈希表,通过数组和链表进行存储。数组索引表示该点,数组中的链表存放该点连同的一系列点以及对应边的权重。
1、Breadth First Search
广度优先搜索优先搜索离起始点最近的,然后次近,然后次次近,直到找到终点。此处我们使用邻接表来表示图。
public void bfs(int s,int t,LinkedList<Integer>[] adj,int v){
if(s==t) return;
//记录是否被访问
boolean[] visited = new boolean[v];
visited[s] = true;
//逐层添加元素
Queue<Integer> queue = new LinkedList<>();
queue.add(s);
//记录搜索路径
int[] prev = new int[v];
for(int i=0;i<v;++i){
prev[i] = -1;
}
while(queue.size()!=0){
int w = queue.poll();
for(int i=0;i<adj[w].size();i++){
int q = adj[w].get(i);
if(!visited[q]){
prev[q] = w;
if(q==t){
print(prev,s,t);
return;
}
visited[q] = true;
queue.add(q);
}
}
}
}
private void print(int[] prev,int s,int s){
if(prev[t] != -1 && t!=s){
print(prev,s,prev[t]);
}
System.out.print(t + "");
}
2、Depth First Search
深度优先搜索便是“可能我撞了男墙才会回头吧”的搜索方式,其利用了回溯算法的思想,可以用递归实现也可以通过栈来辅助实现。回溯的艺术是来源于return的执行,回溯在for循环邻接表的LinkedList数组中实现。
boolean found = false;
public void dfs(int s,int t,int v,LinkedList<Integer>[] adj){
found = false;
boolean[] visited = new boolean[v];
int prev[] = new int[v];
for(int i=0;i<v;i++){
prev[i] = -1;
}
recurDfs(s,t,v,visited,prev,adj);
}
private void recurDfs(int w,int t,int v,boolean[] visited,int[] prev,LinkedList<Integer>[] adj){
if(found == true) return;
visited[w] = true;
if(w==t){
found = true;
return;
}
for(int i=0;i<adj[w].size();i++){
int q = adj[w].get(i);
if(!visited[q]){
prev[q] = w;
recurDfs(q,t,v,visited,prev,adj)
}
}
}
3、最小生成树
只有连通图才有最小生成树,最小生成树一般采用Prom算法和Kruskal算法完成,这两种算法的核心思想是贪心算法。
(1)Prim算法
从某一个顶点开始构建生成树,每次将代价最小的新顶点纳入生成树,直到所有顶点都纳入为止。
算法思路:
- 需要使用两个数组isJoin和lowCost,isJoin标记各个节点是否加入树,lowCost记录各节点加入树的最低代价。
- 遍历lowCost,找到与起始点(已有生成树)相连代价最小的顶点,加入到树中
- lowCost需要动态维护,每次加入最小代价顶点到树时更新lowCost。
(2)Kruskal算法
每次选择一条权值最小的边,使这条边的两头连通(如果原本连通就不选),直到所有顶点都连通。
算法思路:
- 将各个边按权值进行排序,按从小到大进行连通
- 每次连通进行是否已经连通的检查
4、最短路径
(1)Dijkstra
Dijkstra算法得到的结果是起始点到各个顶点的最短路径,基于贪心的思想,始终保持当前迭代解为当前最优解。
- 将其余点标记为未访问,遍历未访问集合,算出起始点到各个顶点的距离
- 找到可达到的最短的顶点作为已访问顶点
- 重新计算顶点从该点到达其它可到达顶点的距离,更新顶点到各顶点的距离数组
- 继续找可达到的最短的顶点作为已访问顶点循环执行上一步操作
(2)Floyd
Floyd得到的结果是图中每两个顶点的最短路径,采用动态规划的处理思想,每个阶段引入一个中转点,查看通过中转点能否缩小各个顶点之间的距离,下一阶段的解基于上一阶段的结果。
for(int k=0;k<v;k++){
for(int i=0;i<v;i++){
for(int i=1;j<v;j++){
if(dist[i][j]>dist[i][k]+dist[k][j]){
dist[i][j] = dist[i][k] + dist[i][j];
}
}
}
}
k是加入的顶点,dist[i][j]是顶点ij之间的最短距离。
5、旅行商问题
TSP问题(Traveling Salesman Problem,旅行商问题),由威廉哈密顿爵士和英国数学家克克曼T.P.Kirkman于19世纪初提出。问题描述如下:有若干个城市,任何两个城市之间的距离都是确定的,现要求一旅行商从某城市出发必须经过每一个城市且只在一个城市逗留一次,最后回到出发的城市,问如何事先确定一条最短的线路已保证其旅行的费用最少?
public class PathPlaningService
{
public string SA_GetPath()
{
long[,] distance = { {0,250,120,50},
{250,0,100,200},
{120,100,0,80},
{50,200,80,0}};
//参数准备
PathPlaning pathPlaning = new PathPlaning(cities,distance);
int[] OldPath = new int[pathPlaning.path.Length];
Array.Copy(pathPlaning.path,OldPath,pathPlaning.path.Length);
long F_Old = pathPlaning.GetDistanceOfPath(OldPath);
//初始化退火模型参数
double T = 1000;
double T_Min = 0.00001;
double ratio = 0.95;
int T_Times = 500;
int Max_Times = 10000;
//模拟退火过程
for(int i = 0;i < Max_Times; i++)
{
for(int j = 0;j < T_Times; j++)
{
//产生新解
int[] newPath = pathPlaning.ChangePathOrder(OldPath);
long F_New = pathPlaning.GetDistanceOfPath(newPath);
//判断是否接受新解
if(F_New < F_Old)
{
F_Old = F_New;
Array.Copy(newPath,OldPath,newPath.Length);
}
else
{
Random r = new Random();
if(r.NextDouble()> Math.Exp((F_Old - F_New)/T))
{
F_Old = F_New;
Array.Copy(newPath, OldPath, newPath.Length);
}
}
}
T = T * ratio;//降温
if(T < T_Min)
{
break;
}
}
return "路径:" + string.Join("→", OldPath) + " 路径公里数:" + F_Old.ToString();
}
}
四、二分查找
二分查找是一个复杂度为O(logn)的高效算法,若数的量级为n=232也只需要执行32次比较操作便可以找到相应的数据。二分查找的高效是建立在数组有序的基础上,而维护数组有序的成本较高,所以二分查找的实际应用较为局限。
public int binarySearch(int[] a,int value){
int len = a.length;
int low = 0;
int high = len-1;
while(low<=high){
int mid = low + ((high-low)>>1);
if(a[mid] == value){
return a[mid];
}
else if(a[mid]<value){
low = mid + 1;
}
else{
high = mid - 1;
}
}
return -1;
}
因为high一直向左走,low向右走,若数组中不存在该元素,则两者终究会相遇,所以[low<=high]是循环结束的终止条件。
五、排序算法
1、冒泡排序
2、插入排序
3、选择排序
4、快速排序
5、归并排序
六、数组与字符串常用方法[Java]
1、Arrays
二分查找:
int binarySearch(type[ ] a,type key)
int binarySearch(type[ ] a,int fromIndex,int toIndex,ype key)
复制:
type[ ] copyOf(type[ ] original,int length)
type[ ] copyOfRange(type[ ] original,int from,int to)
填充:
void fill(type[ ] a,type val)
void fill(type[ ] a,int fromIndex,int toIndex,type val)
排序:
void sort(type[ ] a)
void sort(type[ ] a,int fromIndex,int toIndex)
toString:
String toString(type[ ] a)
2、String
3、StringBuilder
七、Java常用集合
凡是实现了Collection接口的(List、Set、Queue)都可以调用toArray()方法转换为对象数组,大小都是通过size()方法进行获取。
1、List
Java中的List主要有ArrayList和LinkedList,两者皆实现了List接口(List接口实现了Collection接口)。
增加:add(Type t)、add(int index,Type t)
删除:remove(int index)
获取:get(int index)
替换:set(int index,Type t)
包含:contains(Type t)
2、Queue
Queue<Type> queue = new LinkedList<>();
一般我们采取上述方式进行队列对象的新建。
增加:offer(Type t)、add(Type t)
删除:poll()
获取队列头元素:peek()
3、Stack
Stack<Type> stack = new Stack<>()
增加:push()
删除:pop()
获取队列头元素:peek()
4、HashSet
Hashset基于HashMap实现,实现了Set接口,支持序列化。Set集合最大的特点是不允许有重复元素。
添加:add(Type t)
删除:remove(Type t)
包含:contains(Type t)
5、HashMap
HashMap底层是数组+链表+红黑树,HashTable底层是数组+链表。HashTable是线程安全的,而HashMap非线程安全。HashMap主要用于存储Key-Value对。
- 红黑树转换:数组大小大于64且节点树大于8,转为红黑树。节点数小于6,红黑树转链表。在节点数较小时红黑树的维护成本较大,O(n)级别查询速度较快。
- 负载因子:0.75经验值,当负载因子过大(元素个数/HashMap长度超过0.75)时进行扩容