这份教程针对数据结构与算法的基础重点进行梳理,以便快速复习相关知识点以及思想,不求全,而求精。
栈和队列就不说了,栈是先进后出,队列是先进先出。树也有几个专业名词,分别是:高度(从下往上)、深度(从上往下)、层、双亲节点、叶子节点、度(节点分支的个数)。其中树的度指的是各个节点中度的最大值。
一般二叉树的节点声明如下:
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
}
满二叉树和完全二叉树,其第i个节点的孩子节点为2i和2i+1,双亲节点为i/2。
二叉树的遍历是经典的递归算法,分别有前序遍历、中序遍历以及后序遍历。
public void preOrder(TreeNode root){
System.out.println(root.val);
preOrder(root.left);
preOrder(root.right);
}
public void inOrder(TreeNode root){
inOrder(root.left);
System.out.println(root.val);
inOrder(root.right);
}
public void postOrder(TreeNode root){
postOrder(root.left);
postOrder(root.right);
System.out.println(root.val);
}
BFS【层序遍历】需要使用队列进行辅助:
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;
}
}
与BFS不同,深度优先是利用栈作为辅助:
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;
}
}
二叉查找树是一棵左子树小,右子树大的节点。
判断一棵二叉树是否是查找树,要使用区间判断不能直接进行节点间的判断。
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);
}
}
二叉查找树的删除分三种情况:
对于每个子树,左右子树的高度差不超过1。通过左旋和右旋来进行树的结构调整。
B树是同一节点有多个元素的树,如4阶B树为2-3-4树,实现较为困难。
MySQL是底层是以页作为基本存储单元,在页中的数据以单链表+槽(Slot,页目录)的方式存储,而页头会存放Page_Pre和Page_Next指向前后页,从这里便开始采用B+树的数据结构,建立页(双向链表)的索引,即非叶子节点。非叶子节点存储的都是索引列和对应页号,叶子节点存储完整数据。
从JDK1.8开始,HashMap的底层数据结构是由“数组+链表+红黑树组成”:
红黑树是通过变色+左右旋操作使树达到平衡,其根节点和叶子节点为黑色空节点,同一简单路径上相邻元素不能同为红色,且黑色节点数相同,插入的节点为红色。
堆是特殊的完全二叉树,所以一般使用数组来存储,有大顶堆和小顶堆。掌握堆数据结构算法的关键是堆化操作。
以下是大顶堆的堆化操作:
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点的边,其值代表权重。这种存储方式的弊端是不利于存储稀疏图,会浪费较多的存储空间。(例若用图来表示好友关系,微信用户好几亿,但每个用户的好友只有几百个)。
邻接表类似哈希表,通过数组和链表进行存储。数组索引表示该点,数组中的链表存放该点连同的一系列点以及对应边的权重。
广度优先搜索优先搜索离起始点最近的,然后次近,然后次次近,直到找到终点。此处我们使用邻接表来表示图。
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 + "");
}
深度优先搜索便是“可能我撞了男墙才会回头吧”的搜索方式,其利用了回溯算法的思想,可以用递归实现也可以通过栈来辅助实现。回溯的艺术是来源于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)
}
}
}
只有连通图才有最小生成树,最小生成树一般采用Prom算法和Kruskal算法完成,这两种算法的核心思想是贪心算法。
从某一个顶点开始构建生成树,每次将代价最小的新顶点纳入生成树,直到所有顶点都纳入为止。
算法思路:
每次选择一条权值最小的边,使这条边的两头连通(如果原本连通就不选),直到所有顶点都连通。
算法思路:
Dijkstra算法得到的结果是起始点到各个顶点的最短路径,基于贪心的思想,始终保持当前迭代解为当前最优解。
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之间的最短距离。
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]是循环结束的终止条件。
二分查找:
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)
凡是实现了Collection接口的(List、Set、Queue)都可以调用toArray()方法转换为对象数组,大小都是通过size()方法进行获取。
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)
Queue<Type> queue = new LinkedList<>();
一般我们采取上述方式进行队列对象的新建。
增加:offer(Type t)、add(Type t)
删除:poll()
获取队列头元素:peek()
Stack<Type> stack = new Stack<>()
增加:push()
删除:pop()
获取队列头元素:peek()
Hashset基于HashMap实现,实现了Set接口,支持序列化。Set集合最大的特点是不允许有重复元素。
添加:add(Type t)
删除:remove(Type t)
包含:contains(Type t)
HashMap底层是数组+链表+红黑树,HashTable底层是数组+链表。HashTable是线程安全的,而HashMap非线程安全。HashMap主要用于存储Key-Value对。