暑期实习面试准备--数据结构
数据结构
数据结构
链表
- 链表是一种由节点(Node)组成的线性数据集合,每个节点通过指针指向下一个节点。它是一种由节点组成,并能用于表示序列的数据结构。
- 单链表:每个节点仅指向下一个节点,最后一个节点指向空(null)。
- 双链表:每个节点有两个指针p,n。p指向前一个节点,n指向下一个节点;最后一个节点指向空。
- 循环链表:每个节点指向下一个节点,最后一个节点指向第一个节点。
- 时间复杂度:
- 索引:O(n)
- 查找:O(n)
- 插入:O(1)
- 删除:O(1)
栈
- 栈是一个元素集合,支持两个基本操作:push用于将元素压入栈,pop用于删除栈顶元素。
- 后进先出的数据结构(Last In First Out, LIFO)
- 时间复杂度
- 索引:O(n)
- 查找:O(n)
- 插入:O(1)
- 删除:O(1)
队列
- 队列是一个元素集合,支持两种基本操作:enqueue 用于添加一个元素到队列,dequeue 用于删除队列中的一个元素。
- 先进先出的数据结构(First In First Out, FIFO)。
- 时间复杂度
- 索引:O(n)
- 查找:O(n)
- 插入:O(1)
- 删除:O(1)
树
- 树是无向、联通的无环图。
B-树
b+树
二叉树
- 二叉树是一个树形数据结构,每个节点最多可以有两个子节点,称为左子节点和右子节点。
- 满二叉树(Full Tree):二叉树中的每个节点有 0 或者 2 个子节点。
- 完美二叉树(Perfect Binary):二叉树中的每个节点有两个子节点,并且所有的叶子节点的深度是一样的。
- 完全二叉树:二叉树中除最后一层外其他各层的节点数均达到最大值,最后一层的节点都连续集中在最左边。
二叉查找树
- 二叉查找树(BST)是一种二叉树。其任何节点的值都大于等于左子树中的值,小于等于右子树中的值。
- 时间复杂度
- 索引:O(log(n))
- 查找:O(log(n))
- 插入:O(log(n))
- 删除:O(log(n))
字典树
- 字典树,又称为基数树或前缀树,是一种用于存储键值为字符串的动态集合或关联数组的查找树。树中的节点并不直接存储关联键值,而是该节点在树中的位置决定了其关联键值。一个节点的所有子节点都有相同的前缀,根节点则是空字符串。
树状数组
- 树状数组,又称为二进制索引树(Binary Indexed Tree,BIT),其概念上是树,但以数组实现。数组中的下标代表树中的节点,每个节点的父节点或子节点的下标可以通过位运算获得。数组中的每个元素都包含了预计算的区间值之和,在整个树更新的过程中,这些计算的值也同样会被更新。
- 时间复杂度
- 区间求和:O(log(n))
- 更新:O(log(n))
线段树
- 线段树是用于存储区间和线段的树形数据结构。它允许查找一个节点在若干条线段中出现的次数。
- 时间复杂度
- 区间查找:O(log(n))
- 更新:O(log(n))
堆
- 堆是一种基于树的满足某些特性的数据结构:整个堆中的所有父子节点的键值都满足相同的排序条件。堆分为最大堆和最小堆。在最大堆中,父节点的键值永远大于等于所有子节点的键值,根节点的键值是最大的。最小堆中,父节点的键值永远小于等于所有子节点的键值,根节点的键值是最小的。
- 时间复杂度
- 索引:O(log(n))
- 查找:O(log(n))
- 插入:O(log(n))
- 删除:O(log(n))
- 删除最大值/最小值:O(1)
哈希
- 哈希用于将任意长度的数据映射到固定长度的数据。哈希函数的返回值被称为哈希值、哈希码或者哈希。如果不同的主键得到相同的哈希值,则发生了冲突。
- Hash Map:hash map 是一个存储键值间关系的数据结构。HashMap 通过哈希函数将键转化为桶或者槽中的下标,从而便于指定值的查找。
- 冲突解决
- 链地址法(Separate Chaining):在链地址法中,每个桶(bucket)是相互独立的,每一个索引对应一个元素列表。处理HashMap 的时间就是查找桶的时间(常量)与遍历列表元素的时间之和。
- 开放地址法(Open Addressing):在开放地址方法中,当插入新值时,会判断该值对应的哈希桶是否存在,如果存在则根据某种算法依次选择下一个可能的位置,直到找到一个未被占用的地址。开放地址即某个元素的位置并不永远由其哈希值决定。
图
- 图是G =(V,E)的有序对,其包括顶点或节点的集合 V 以及边或弧的集合E,其中E包括了两个来自V的元素(即边与两个顶点相关联 ,并且该关联为这两个顶点的无序对)。
- 无向图:图的邻接矩阵是对称的,因此如果存在节点 u 到节点 v 的边,那节点 v 到节点 u 的边也一定存在。
- 有向图:图的邻接矩阵不是对称的。因此如果存在节点 u 到节点 v 的边并不意味着一定存在节点 v 到节点 u 的边。
排序
希尔排序
不稳定
最好:onlogn
最差:o2
public static void shellSortSmallToBig(int[] data) {
int j = 0;
int temp = 0;
for (int increment = data.length / 2; increment > 0; increment /= 2) {
System.out.println("increment:" + increment);
for (int i = increment; i < data.length; i++) {
// System.out.println("i:" + i);
temp = data[i];
for (j = i - increment; j >= 0; j -= increment) {
// System.out.println("j:" + j);
// System.out.println("temp:" + temp);
// System.out.println("data[" + j + "]:" + data[j]);
if (temp < data[j]) {
data[j + increment] = data[j];
} else {
break;
}
}
data[j + increment] = temp;
}
for (int i = 0; i < data.length; i++)
System.out.print(data[i] + " "); }
}
public static void main(String[] args) {
int[] data = new int[] { 26, 53, 67, 48, 57, 13, 48, 32, 60, 50 };
shellSortSmallToBig(data);
System.out.println(Arrays.toString(data));
}
快速排序
- 稳定:否
- 时间复杂度
- 最优:O(nlog(n))
- 最差:O(n^2)
- 平均:O(nlog(n)
public static int partition(int []array,int lo,int hi){ //固定的切分方式 int key=array[lo]; while(lo<hi){ while(array[hi]>=key&&hi>lo){//从后半部分向前扫描 hi--; } array[lo]=array[hi]; while(array[lo]<=key&&hi>lo){从前半部分向后扫描 lo++; } array[hi]=array[lo]; } array[hi]=key; return hi; } public static void sort(int[] array,int lo ,int hi){ if(lo>=hi){ return ; } int index=partition(array,lo,hi); sort(array,lo,index-1); sort(array,index+1,hi); }
合并排序
- 合并排序是一种分治算法。这个算法不断地将一个数组分为两部分,分别对左子数组和右子数组排序,然后将两个数组合并为新的有序数组。
- 稳定:是
- 时间复杂度:
- 最优:O(nlog(n))
- 最差:O(nlog(n))
- 平均:O(nlog(n))
桶排序
- 桶排序是一种将元素分到一定数量的桶中的排序算法。每个桶内部采用其他算法排序,或递归调用桶排序。
- 时间复杂度
- 最优:Ω(n + k)
- 最差: O(n^2)
- 平均:Θ(n + k)
基数排序
- 基数排序类似于桶排序,将元素分发到一定数目的桶中。不同的是,基数排序在分割元素之后没有让每个桶单独进行排序,而是直接做了合并操作。
- 时间复杂度
- 最优:Ω(nk)
- 最差: O(nk)
- 平均:Θ(nk)
图算法
深度优先搜索
- 深度优先搜索是一种先遍历子节点而不回溯的图遍历算法。
- 时间复杂度:O(|V| + |E|)
广度优先搜索
- 广度优先搜索是一种先遍历邻居节点而不是子节点的图遍历算法。
- 时间复杂度:O(|V| + |E|)
拓扑排序
- 拓扑排序是有向图节点的线性排序。对于任何一条节点 u 到节点 v 的边,u 的下标先于 v。
- 时间复杂度:O(|V| + |E|)
Dijkstra算法
- Dijkstra 算法是一种在有向图中查找单源最短路径的算法。
- 时间复杂度:O(|V|^2)
public
class
Dijkstra {
private
static
int
N =
1000
;
private
static
int
[][] Graph = {
{
0
,
1
,
5
, N, N, N, N, N, N },
{
1
,
0
,
3
,
7
,
5
, N, N, N, N },
{
5
,
3
,
0
, N,
1
,
7
, N, N, N },
{ N,
7
, N,
0
,
2
, N,
3
, N, N },
{ N,
5
,
1
,
2
,
0
,
3
,
6
,
9
, N },
{ N, N,
7
, N,
3
,
0
, N,
5
, N },
{ N, N, N,
3
,
6
, N,
0
,
2
,
7
},
{ N, N, N, N,
9
,
5
,
2
,
0
,
4
},
{ N, N, N, N, N, N,
7
,
4
,
0
} };
public
static
void
main(String[] args) {
dijkstra(
0
, Graph);
}
/**
* Dijkstra最短路径。
* 即图中"节点vs"到其它各个节点的最短路径。
* @param vs 起始节点
* @param Graph 图
*/
public
static
void
dijkstra(
int
vs,
int
[][] Graph) {
int
NUM = Graph[
0
].length;
// 前驱节点数组
int
[] prenode =
new
int
[NUM];
// 最短距离数组
int
[] mindist =
new
int
[NUM];
// 该节点是否已经找到最短路径
boolean
[] find =
new
boolean
[NUM];
int
vnear =
0
;
for
(
int
i =
0
; i < mindist.length; i++) {
prenode[i] = i;
mindist[i] = Graph[vs][i];
find[i] =
false
;
}
find[vs] =
true
;
for
(
int
v =
1
; v < Graph.length; v++) {
// 每次循环求得距离vs最近的节点vnear和最短距离min
int
min = N;
for
(
int
j =
0
; j < Graph.length; j++) {
if
(!find[j] && mindist[j] < min) {
min = mindist[j];
vnear = j;
}
}
find[vnear] =
true
;
// 根据vnear修正vs到其他所有节点的前驱节点及距离
for
(
int
k =
0
; k < Graph.length; k++) {
if
(!find[k] && (min + Graph[vnear][k]) < mindist[k]) {
prenode[k] = vnear;
mindist[k] = min + Graph[vnear][k];
}
}
}
for
(
int
i =
0
; i < NUM; i++) {
System.out.println(
"v"
+ vs +
"...v"
+ prenode[i] +
"->v"
+ i +
", s="
+ mindist[i]);
}
}
Bellman-Ford算法
- Bellman-Ford 是一种在带权图中查找单一源点到其他节点最短路径的算法。
- 虽然时间复杂度大于 Dijkstra 算法,但它可以处理包含了负值边的图。
- 时间复杂度:
- 最优:O(|E|)
- 最差:O(|V||E|)
Floyd-Warshall 算法
- Floyd-Warshall 算法是一种在无环带权图中寻找任意节点间最短路径的算法。
- 该算法执行一次即可找到所有节点间的最短路径(路径权重和)。
- 时间复杂度:
- 最优:O(|V|^3)
- 最差:O(|V|^3)
- 平均:O(|V|^3)
最小生成树算法
- 最小生成树算法是一种在无向带权图中查找最小生成树的贪心算法。换言之,最小生成树算法能在一个图中找到连接所有节点的边的最小子集。
- 时间复杂度:O(|V|^2)
- public class Main {
- public static void main(String[] args) throws Exception {
- int MAX = Integer.MAX_VALUE;
- int[][] map = new int[][]{
- {0,10,MAX,MAX,MAX,11,MAX,MAX,MAX},
- {10,0,18,MAX,MAX,MAX,16,MAX,12},
- {MAX,MAX,0,22,MAX,MAX,MAX,MAX,8},
- {MAX,MAX,22,0,20,MAX,MAX,16,21},
- {MAX,MAX,MAX,20,0,26,MAX,7,MAX},
- {11,MAX,MAX,MAX,26,0,17,MAX,MAX},
- {MAX,16,MAX,MAX,MAX,17,0,19,MAX},
- {MAX,MAX,MAX,16,7,MAX,19,0,MAX},
- {MAX,12,8,21,MAX,MAX,MAX,MAX,0}
- };
- int sum = prim(map);
- System.out.println(sum);
- }
- public static int prim(int[][] arr){
- //统计最小的权
- int sum = 0;
- //当前最小生成树所能到达的顶点的最小权数组
- int[] costs = new int[9];
- //当前各个顶点对应的起点
- int[] startPoint = new int[9];
- //初始化
- for(int i =1;i<9;i++){
- //所有点的起点赋值为0
- startPoint[i] = 0;
- //以0为起点到达各个顶点的权值
- costs[i] = arr[0][i];
- }
- //挑选剩余的8个顶点
- for(int i = 1;i<9;i++){
- //记录当前costs里面的最小权值是多少
- int min = Integer.MAX_VALUE;
- //记录当前costs里面的最小权值对应的数组下标,即顶点
- //(数组[顶点]=该顶点对应的起点)
- int minIndex = 0;
- //遍历costs
- for(int j=1;j<9;j++){
- int temp = costs[j];
- //costs[j]==0代表节点j已加入MST
- if(temp!=0&&temp < min){
- min = temp;
- minIndex = j;
- }
- }
- sum+=min;
- //将已加入MST的对应的权值赋值为0
- costs[minIndex] = 0;
- //选定了新的顶点到MST后,树到达各顶点的最小开销和起点将更新
- //更新costs和startPoint
- for(int k=0;k<9;k++){
- //用minIndex顶点到各个顶点的权值比较costs数组的值,若较小则替换,并更新起点为minIndex
- int newCost = arr[minIndex][k];
- if(newCost!=0&&newCost<costs[k]){
- costs[k] = newCost;
- //更新K的起点为minIndex
- startPoint[k] = minIndex;
- }
- }
- }
- return sum;
- }
- }
Kruskal 算法
- Kruskal 算法也是一个计算最小生成树的贪心算法,但在 Kruskal 算法中,图不一定是连通的。
- 时间复杂度:O(|E|log|V|)
求环路
public class Solution {
ListNode EntryNodeOfLoop(ListNode h){
if(h == null || h.next == null)
return null;
ListNode slow = h;
ListNode fast = h;
while(fast != null && fast.next != null ){
slow = slow.next;
fast = fast.next.next;
if(slow == fast){
ListNode p=h;
ListNode q=slow;//相当于让q指向了m1
while(p != q){
p = p.next;
q = q.next;
}
if(p == q)
return q;
}
}
return null;
}
位运算
- 位运算即在比特级别进行操作的技术。使用位运算技术可以带来更快的运行速度与更小的内存使用。
- 测试第 k 位:s & (1 << k);
- 设置第k位:s |= (1 << k);
- 关闭第k位:s &= ~(1 << k);
- 切换第k位:s ^= (1 << k);
- 乘以2n:s << n;
- 除以2n:s >> n;
- 交集:s & t;
- 并集:s | t;
- 减法:s & ~t;
- 提取最小非0位:s & (-s);
- 提取最小0位:~s & (s + 1);
- 交换值:x ^= y; y ^= x; x ^= y;
树算法
树的递归和非递归遍历
- import java.util.Stack;
- public class BinaryTree {
- protected Node root;
- public BinaryTree(Node root) {
- this.root = root;
- }
- public Node getRoot() {
- return root;
- }
- /** 构造树 */
- public static Node init() {
- Node a = new Node('A');
- Node b = new Node('B', null, a);
- Node c = new Node('C');
- Node d = new Node('D', b, c);
- Node e = new Node('E');
- Node f = new Node('F', e, null);
- Node g = new Node('G', null, f);
- Node h = new Node('H', d, g);
- return h;// root
- }
- /** 访问节点 */
- public static void visit(Node p) {
- System.out.print(p.getKey() + " ");
- }
- /** 递归实现前序遍历 */
- protected static void preorder(Node p) {
- if (p != null) {
- visit(p);
- preorder(p.getLeft());
- preorder(p.getRight());
- }
- }
- /** 递归实现中序遍历 */
- protected static void inorder(Node p) {
- if (p != null) {
- inorder(p.getLeft());
- visit(p);
- inorder(p.getRight());
- }
- }
- /** 递归实现后序遍历 */
- protected static void postorder(Node p) {
- if (p != null) {
- postorder(p.getLeft());
- postorder(p.getRight());
- visit(p);
- }
- }
- /**********************************************************************************************/
- /** 非递归实现前序遍历 */
- protected static void iterativePreorder(Node p) {
- Stack<Node> stack = new Stack<Node>();
- if (p != null) {
- stack.push(p);
- while (!stack.empty()) {
- p = stack.pop();
- visit(p);
- if (p.getRight() != null)
- stack.push(p.getRight());
- if (p.getLeft() != null) //为什么p.getLeft() 在后,getRight()在前应为while 循环第一句就是pop visit所以要把left放上,先访问。之中方法是即压即访问法。
- stack.push(p.getLeft());
- }
- }
- }
- /** 非递归实现中序遍历 */ //思路与上面iterativePreorder 一致。
- protected static void iterativeInorder(Node p) {
- Stack<Node> stack = new Stack<Node>();
- while (p != null) {
- while (p != null) {
- if (p.getRight() != null)
- stack.push(p.getRight());// 当前节点右子入栈
- stack.push(p);// 当前节点入栈
- p = p.getLeft();
- }
- p = stack.pop();
- while (!stack.empty() && p.getRight() == null) {
- visit(p);
- p = stack.pop();
- }
- visit(p);
- if (!stack.empty())
- p = stack.pop();
- else
- p = null;
- }
- }
- /*******************************************************************************************/
- /*******************************************************************************************/
- /** 非递归实现前序遍历2 */
- protected static void iterativePreorder2(Node p) {
- Stack<Node> stack = new Stack<Node>();
- Node node = p;
- while (node != null || stack.size() > 0) {
- while (node != null) {//压入所有的左节点,压入前访问它。左节点压入完后pop访问右节点。像这样算法时思考规律性的东西在哪。不管哪个节点都要压所节点判断右节点。
- visit(node);
- stack.push(node);
- node = node.getLeft();
- }
- if (stack.size() > 0) {//
- node = stack.pop();
- node = node.getRight();
- }
- }
- }
- /** 非递归实现中序遍历2 */
- protected static void iterativeInorder2(Node p) {
- Stack<Node> stack = new Stack<Node>();
- Node node = p;
- while (node != null || stack.size() > 0) {
- while (node != null) {
- stack.push(node);
- node = node.getLeft();
- }
- if (stack.size() > 0) {
- node = stack.pop();
- visit(node); //与iterativePreorder2比较只有这句话的位置不一样,弹出时再访问。
- node = node.getRight();
- }
- }
- }
- /*******************************************************************************************/
- /** 非递归实现后序遍历 */
- protected static void iterativePostorder(Node p) {
- Node q = p;
- Stack<Node> stack = new Stack<Node>();
- while (p != null) {
- // 左子树入栈
- for (; p.getLeft() != null; p = p.getLeft())
- stack.push(p);
- // 当前节点无右子或右子已经输出
- while (p != null && (p.getRight() == null || p.getRight() == q)) {
- visit(p);
- q = p;// 记录上一个已输出节点
- if (stack.empty())
- return;
- p = stack.pop();
- }
- // 处理右子
- stack.push(p);
- p = p.getRight();
- }
- }
- /** 非递归实现后序遍历 双栈法 */
- protected static void iterativePostorder2(Node p) {//理解左子树 右子树 根递归性质,把它运用到循环当中去。
- Stack<Node> lstack = new Stack<Node>();//左子树栈
- Stack<Node> rstack = new Stack<Node>();//右子树栈
- Node node = p, right;
- do {
- while (node != null) {
- right = node.getRight();
- lstack.push(node);
- rstack.push(right);
- node = node.getLeft();
- }
- node = lstack.pop();
- right = rstack.pop();
- if (right == null) {
- visit(node);
- } else {
- lstack.push(node);
- rstack.push(null);
- }
- node = right;
- } while (lstack.size() > 0 || rstack.size() > 0);
- }
- /** 非递归实现后序遍历 单栈法*/
- protected static void iterativePostorder3(Node p) {
- Stack<Node> stack = new Stack<Node>();
- Node node = p, prev = p;
- while (node != null || stack.size() > 0) {
- while (node != null) {
- stack.push(node);
- node = node.getLeft();
- }
- if (stack.size() > 0) {
- Node temp = stack.peek().getRight();
- if (temp == null || temp == prev) {
- node = stack.pop();
- visit(node);
- prev = node;
- node = null;
- } else {
- node = temp;
- }
- }
- }
- }
- /** 非递归实现后序遍历4 双栈法*/
- protected static void iterativePostorder4(Node p) {
- Stack<Node> stack = new Stack<Node>();
- Stack<Node> temp = new Stack<Node>();
- Node node = p;
- while (node != null || stack.size() > 0) {
- while (node != null) {
- temp.push(node);
- stack.push(node);
- node = node.getRight();
- }
- if (stack.size() > 0) {
- node = stack.pop();
- node = node.getLeft();
- }
- }
- while (temp.size() > 0) {//把插入序列都插入到了temp。
- node = temp.pop();
- visit(node);
- }
- }
- /**
- * @param args
- */
- public static void main(String[] args) {
- BinaryTree tree = new BinaryTree(init());
- System.out.print(" 递归遍历 \n");
- System.out.print(" Pre-Order:");
- preorder(tree.getRoot());
- System.out.print(" \n In-Order:");
- inorder(tree.getRoot());
- System.out.print("\n Post-Order:");
- postorder(tree.getRoot());
- System.out.print(" \n非递归遍历");
- System.out.print(" \n Pre-Order:");
- iterativePreorder(tree.getRoot());
- System.out.print("\n Pre-Order2:");
- iterativePreorder2(tree.getRoot());
- System.out.print(" \n In-Order:");
- iterativeInorder(tree.getRoot());
- System.out.print("\n In-Order2:");
- iterativeInorder2(tree.getRoot());
- System.out.print("\n Post-Order:");
- iterativePostorder(tree.getRoot());
- System.out.print("\n Post-Order2:");
- iterativePostorder2(tree.getRoot());
- System.out.print("\n Post-Order3:");
- iterativePostorder3(tree.getRoot());
- System.out.print("\n Post-Order4:");
- iterativePostorder4(tree.getRoot());
- }
- }
- class Node {
- private char key;
- private Node left, right;
- public Node(char key) {
- this(key, null, null);
- }
- public Node(char key, Node left, Node right) {
- this.key = key;
- this.left = left;
- this.right = right;
- }
- public char getKey() {
- return key;
- }
- public void setKey(char key) {
- this.key = key;
- }
- public Node getLeft() {
- return left;
- }
- public void setLeft(Node left) {
- this.left = left;
- }
- public Node getRight() {
- return right;
- }
- public void setRight(Node right) {
- this.right = right;
- }
- }