目录
队列 Queue FIFO(先进先出)front 移除元素 back 插入元素
基本的概念
数据结构表示数据在计算机中的存储和组织形式,主要描述数据元素之间和位置关系等。选择适当的数据结构可以提高计算机程序的运行效率(时间复杂度)和存储效率(空间复杂度)。
数据结构+算法=程序
数组 Array
数组是最简单、也是使用最广泛的数据结构。栈、队列等其他数据结构均由数组演变而来。
数组的基本操作
Insert——在指定索引位置插入一个元素
Get——返回指定索引位置的元素
Delete——删除指定索引位置的元素
Size——得到数组所有元素的数量
面试笔试中关于数组的常见问题
1、寻找数组中第二小的元素
方法:冒泡排序(也可以其他排序)求a[1]
2、找到数组中第一个不重复出现的整数
方法:
一、使用哈希表(HashMap),不存在则map.put(a[i],1),存在则map.put(a[i],map.get(i)+1),最后找到第一个value为1的值的key
二、类似于选择排序 双重for循环中将a[i]与a[j]比较(i!=j),相等则进入下一个 i 循环,所有的都不相等则返回这个数
3、删除有序数组中的重复元素
方法:
一、使用set集合,数组元素放入set集合中,利用set集合的不重复特性
二、使用list集合,依次放入list集合之前,判断是否有该元素,有则不添加
三、使用纯数组,思路如下:
比较 slow 和 qfast位置的元素是否相等。
如果相等,fast后移 1 位 如果不相等,将 fast 位置的元素复制到 slow+1 位置上,slow 后移一 位,fast后移 1 位
重复上述过程,直到 fast 等于数组长度。
返回 slow + 1,即为新数组长度。
//3.3 删除排序数组中的重复项
public static int[] removeDuplicate3(int[] array){
int slow=0;
int fast=1;
// int[] array1 = Arrays.stream(array).distinct().toArray();
// System.out.println(Arrays.toString(array1));
while(fast<array.length){
if(array[slow]!=array[fast]){
array[slow+1]=array[fast];
slow++;
}
fast++;
}
int[] a=new int[slow+1];
System.out.println(Arrays.toString(array));
//复制数组元素从0到slow+1
System.arraycopy(array,0,a,0,slow+1);
System.out.println(Arrays.toString(a));
return a;
}
4、合并两个有序数组
方法:b[j]依次 赋值在a[i](i=a.length-b.length)后面,冒泡排序(其他排序也行)
5、重新排列数组中的正值和负值
情况一:正负交替 1 -2 4 -3 5 -6 类型于快速排序
方法:(a,b都从头开始)一个指针a指向偶数位,一个指针b指向奇数位,当a指向的数值为负数时,停止,当b指向的数值为正数时,停止,然后交换两个数值,直到最后有一个指针超出了数组范围
情况二:负数一组在前正数一组在后不改变原来负数、正数的序号(3 -2 -8 2 9 -1)->(-2 -8 -1 3 2 9)
方法:
一、new一个新数组,第一次for循环添加负数,第二次for循环添加正数,再重新赋给原数组
二、(类似于冒泡排序)将第一个小于0的数找出来a[i],然后a[i-1]->a[0]前面的数组的正数进行交换,直到碰到一个负数,则进行下一次循环
栈 stack LIFO(后进先出)
栈的基本操作
Push——在顶部插入一个元素
Pop——返回并移除栈顶元素
isEmpty——如果栈为空,则返回true
Top——返回顶部元素,但并不移除它
面试笔试中关于栈的常见问题
1、使用栈计算后缀表达式
“ A B - C D E / F - * + G + ” =>> A - B + C *( D / E - F )+ G
在运算过程中,首先创建一个 “操作数栈” 。
1、从左向右扫描,扫描到一个操作数,便将其压入栈顶。
2、扫描到运算符,将靠近栈顶的两个元素弹出,第一个弹出在运算符右侧,第二个弹出在运算符左侧,最后将结果压入栈顶。
2、对栈的元素进行排序
从主栈中依次弹出栈顶元素压入辅助栈,每当将要压入的元素使得辅助栈中的元素不是升序排列,就将辅助栈里的元素重新压入原始栈,直到辅助栈里的元素都小于当前要压入的元素,然后再压入当前元素。
//stack1为主栈,stack2为辅助栈
stack<int> sort(stack<int> stack1)
{
stack<int> stack2;
while (!stack1.empty())
{
int temp = stack1.top();
stack1.pop();
//如果辅助栈不为空且当前元素比辅助栈栈顶元素小,则将辅助栈中元素弹出压入主栈中
while (!stack2.empty() && stack2.top() > temp)
{
stack1.push(stack2.top());
stack2.pop();
}
//如果辅助栈为空或者当前元素比辅助栈栈顶元素大,则将当前元素直接压入辅助栈中
stack2.push(temp);
}
return stack2;
}
3、判断表达式是否括号平衡 开分隔符 " ( [ { " 闭分隔符 " ) ] } "
1.创建一个栈
2.当(当前字符不等于输入的结束字符)
(1)如果当前字符不是匹配的字符,判断栈内是否为空,如果栈为空,括号必然不完整
(2)如果字符是一个开分隔符,那么将其入栈
(3)如果字符是一个闭分隔符,,且栈不为空(如果 栈为空,则左括号少了)
则判断是否匹配,匹配则判断下一个字符
(4)栈结束后判断是否为空,不为空则括号匹配错误 (左括号多了)
public static boolean isValid(String s) {
//声明匹配词典
Map<Character, Character> map = new HashMap<>();
map.put(')', '(');
map.put(']', '[');
map.put('}', '{');
//创建栈
Stack<Character> stack = new Stack<>();
for (char ch : s.toCharArray()) {
//开分隔符入栈
if (ch == '(' || ch == '[' || ch == '{') {
stack.push(ch);
}
//出栈并且栈非空进行匹配
else if(stack.isEmpty() || (map.get(ch)!=null&&stack.pop() != map.get(ch))){
return false;
}
}
//如果栈非空则括号匹配错误
return stack.isEmpty();
}
队列 Queue FIFO(先进先出)front 移除元素 back 插入元素
与栈相似,队列是另一种顺序存储元素的线性数据结构。
队列的基本操作
Enqueue() —— 在队列尾部插入元素
Dequeue() ——移除队列头部的元素
isEmpty()——如果队列为空,则返回true
Top() ——返回队列的第一个元素
面试笔试中关于队列的常见问题
1、使用队列表示栈 (原队列和辅助队列)
方法:将n个元素入队,把前n-1个元素加入到辅助队列中,将原队列的队头元素移除,交换两个队列
2、对队列的前k个元素倒序 (需要一个新队列和一个栈)
方法:将前k个元素入栈,再将栈中元素入新队列中,最后将原队列的剩余元素入新队列中
3、使用队列生成从1到n的二进制数
方法:利用栈先进后出,队列先进先出。
十进制数N转换成d进制数原理:
while(n!=0){
//队列不能用push
queue.add(n%2);
//push 为栈所有的
stack.push(n%2);
n=n/2;
}
链 LinkedList
链表就像一个节点链,其中每个节点包含着数据和指向后续节点的指针。 链表还包含一个头指针,它指向链表的第一个元素,但当列表为空时,它指向null或无具体内容。
链表包括以下类型:
单链表(单向) 双向链表(双向)
链表的基本操作:
addFirst- 在链表的末尾插入指定元素
addLast - 在链接列表的开头/头部插入指定元素
remove - 从链接列表中删除指定元素
removeFirst - 删除链接列表的第一个元素
get - 从链表中返回指定元素
isEmpty - 如果链表为空,则返回true
面试笔试中关于链表的常见问题
1、反转链表
方法:实现原理就是把链表节点一个个入栈,当全部入栈完之后再一个个出栈,出栈的时候在把出栈的结点串成一个新的链表。
import java.util.Stack;
public class Solution {
public ListNode ReverseList(ListNode head) {
Stack<ListNode> stack= new Stack<>();
//把链表节点全部摘掉放到栈中
while (head != null) {
stack.push(head);
head = head.next;
}
if (stack.isEmpty())
return null;
ListNode node = stack.pop();
ListNode dummy = node;
//栈中的结点全部出栈,然后重新连成一个新的链表
while (!stack.isEmpty()) {
ListNode tempNode = stack.pop();
node.next = tempNode;
node = node.next;
}
//最后一个结点就是反转前的头结点,一定要让他的next
//等于空,否则会构成环
node.next = null;
return dummy;
}
}
2、检测链表中的循环
方法:检测是否有环的思想是
一、给方法传入一个当前节点
二、获得当前节点的下一个节点为慢指针,获得当前节点的下下一个节点为快指针
三、当慢指针追上快指针时 代表有循环回路 否则无循环链表
//设置当前链表的节点LoopNode信息
public class LoopNode {
int data;
LoopNode next = this;
public LoopNode(int data) {
this.data = data;
}
public LoopNode next() { //此方法显示下一个节点
return this.next;
}
public int getData() { //此方法 获取当前节点的值
return this.data;
}
//删除当前节点的下一个节点
public void remove(){
// 取当前节点的下下一个节点
if(this.next.next == null){
this.next = this;
}else{
Node nextNext = this.next.next;
this.next = nextNext;
}
}
public void after(LoopNode node) { //插入节点
// 取出一个点为下下一个节点
LoopNode nextNext = this.next;
// 把新节点作为当前节点的下一个节点
this.next = node;
// 再把下下节点 赋给新节点的下一个节点
node.next = nextNext;
}
}
public class TestLoopNode {
public static void main(String[] args) {
LoopNode l1 = new LoopNode(1);
LoopNode l2 = new LoopNode(2);
LoopNode l3 = new LoopNode(3);
l1.after(l2);
l2.after(l3);
System.out.println(isLoop(l1));
}
// 判断是否有环 head从当前节点开始寻找
public static String isLoop(LoopNode head){
LoopNode slow = head;
LoopNode fast = head;
//如果存在空节点 ,说明不存在环
if(head == null){
return "无环";
}
//试想如果一个环,你是找不到空节点的,两个步长不一致的指针迟早要相遇
while(slow !=null && fast != null){
slow = slow.next;
fast = fast.next.next;
if(slow == fast){
return "有环";
}
}
return "无环";
}
}
3、返回链表倒数第N个节点
方法:将链表入栈,然后stack.getIndex(stack.size()-n);
4、删除链表中的重复项
方法:将链表存放在set集合中,然后重新放入链表中
二叉树 Binary Tree
树形结构是一种层级式的数据结构,由顶点(节点)和连接它们的边组成。 树类似于图,但区分树和图的重要特征是树中不存在环路。
满二叉树:
树中的每个节点仅包含 0 或 2 个节点。(国际标准)国内的定义是:除了叶结点外每一个结点都有左右子叶且叶子结点都处在最底层的二叉树。很 显然,按照这个定义,上面的图示二叉树就不是满二叉树。
完美二叉树(Perfect Binary Tree):
二叉树中的每个叶节点都拥有两个子节点,并且具有相同的高度。 等价于满二叉树完全二叉树:
除最后一层外,每一层上的结点数均达到最大值;在最后一层上只缺少右边的若干结点。- 平衡二叉树:是一棵空树或它的任意节点的左右两个子树的高度差的绝对值不超过1(平衡因子=左子树-右子树的高度 :-1 0 1)
- 红黑树(平衡二叉树):每节点五元素==>>颜色、键值、左子节点、右子节的、父节点 。(如果一个节点是红的,则它的两个儿子都是黑的 应用:map和set、linux文件管理)
- 性质:1、从根到叶结点的最长路径不大于最短路径的2倍 2、有n个内部结点的红黑树的高度 h<= 2log2(n+1) 3、新插入红黑树的结点初始着色为红色
- 扩充二叉树:是对已有二叉树的扩充,扩充后的二叉树的节点都变为度数为2的分支节点。也就是说,如果原节点的度数为2,则不变,度数为1,则增加一个分支,度数为0的叶子节点则增加两个分支。
性质1 一棵非空二叉树第i层上最多有2的(i-1)次方个节点(i>=1) 2^(i-1)
性质2:一棵深度额为k的二叉树,最多具有2的k次方-1个节点。 2^k-1
性质3:对于一颗非空的二叉树,若叶子结点数为n0,度数为2的节点数为n2,则有n0=n2+1
性质4:具有n个节点的完全二叉树的深度 k=|log(2 n)|+1 底数为2,上数为n
先序遍历:根、左子树、右子树
中序遍历:左子树、根、右子树
后序遍历:左子树、右子树、根
树:总结点数=n0+n1+n2+'''''''+nm
总分支数=0n0+1n1+2n2+''''''+mnm
总结点数=总分支数+1
如果二叉树的结构是满二叉树或者完全二叉树,那么可以考虑使用数组存储。但如果是一般的二叉树则不建议,因为完全化(扩充)后的二叉树,空元素也需要占据空间,但实际上,这些元素并没有实际含义,所以会导致大量的空间浪费,无论什么结构的二叉树,都建议使用链式存储结构。
二叉链表:(一般二叉树推荐)
如果没有孩子节点,则将指针域置为NULL。使用二叉链表结构,一般会创建一个头节点,并将其中的左子树指向根节点,右子树为NULL,这样方便了后续的操作。
三叉链表: 除了含有二叉链表的结构外,还含有一个双亲节点指针域,指向它的双亲节点,典型的空间换时间的方法,如果涉及的操作需要频繁访问双亲节点,就建议使用三叉链表的存储结构
面试笔试中关于树结构的常见问题:
1、求二叉树的高度
方法:(高度:所有的叶子节点到根节点的最大值,深度:根节点到叶子节点的最大值)
一、
- 后序遍历其左子树求树高
- 后序遍历其右子树求树高
- 对这个结点进行下面操作:
- 比较其左右子树的高度大小
- 若左>右,则选择左的高度;反之,选择右的高度
- 将上一步选出的
高度+1
,并作为返回值返回
- 节点的存储结构
-
public class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(int x) { val = x; } }
-
二、非递归方式(层次遍历)采用队列辅助进行层次遍历,然后统计树的高度。/** * (递归方式)求树的高度:从叶子节点到达根节点的最长路径 * @param root * @return */ public int getTreeHeight(TreeNode root) { if (root == null) { return 0; } int leftHeight = getTreeHeight(root.left); int rightHeight = getTreeHeight(root.right); return leftHeight > rightHeight? leftHeight+1 : rightHeight+1; }
/**
* (非递归方式)求树的高度:从叶子节点到达根节点的最长路径
* @param root
* @return
*/
public int non_getTreeHeight(TreeNode root) {
if (root == null) {
return 0;
}
int height = 0, //记录树的高度
count = 0, //进行层级统计
nextCount = 1; //记录每层遍历后新增到队列中的元素数量
Queue<TreeNode> queue = new LinkedList<BinaryTree.TreeNode>();
queue.add(root);
while (!queue.isEmpty()) {
TreeNode node = queue.poll();
count++;
if (node.left!=null) {
queue.add(node.left);
}
if (node.right!=null) {
queue.add(node.right);
}
if (count == nextCount) { //表示该层已经遍历完了
nextCount = queue.size(); //从新获取队列中保存的下一层节点个数
count = 0; //每层计算器清零
height++; //树的高度加1
}
}
return height;
}
2、在二叉搜索树中查找第k个最大值
方法:
要求第K大的树节点,首先要获得树的排序序列,采用中序遍历正好可以获取到树的顺序序列,然后进行第K大节点判断。
//二叉搜索树的第k大节点(树中第k大的数)
public int kthLargest(TreeNode root, int k) {
if (root == null) {
return 0;
}
List<Integer> list = new ArrayList<Integer>();
findKinOrderTraveral(root, list);
int size = list.size();
return list.get(size-k);
}
public void findKinOrderTraveral(TreeNode root, List<Integer> list){
if(root == null) return;
findKinOrderTraveral(root.left,list);
list.add(root.val);
findKinOrderTraveral(root.right,list);
}
3、查找与根节点距离k的节点
方法:
public static void printOut(TreeNode root,int k){
if(k<0||root==null){
return;
}
if(k==0){
System.out.print(root.value+" ");
return;
}
printOut(root.left, k-1);
printOut(root.right, k-1);
}
public static void main(String[] args) {
TreeNode root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.left.left = new TreeNode(4);
root.left.right = new TreeNode(5);
root.right.left = new TreeNode(8);
printOut(root, 2);
}
4、在二叉树中查找给定节点的祖先节点
方法:
class Node
{
int data;
Node left, right, nextRight;
Node(int item)
{
data = item;
left = right = nextRight = null;
}
}
class BinaryTree
{
Node root;
/* If target is present in tree, then prints the ancestors
and returns true, otherwise returns false. */
boolean printAncestors(Node node, int target)
{
/* base cases */
if (node == null)
return false;
if (node.data == target)
return true;
/* If target is present in either left or right subtree
of this node, then print this node */
if (printAncestors(node.left, target)
|| printAncestors(node.right, target))
{
System.out.print(node.data + " ");
return true;
}
/* Else return false */
return false;
}
/* Driver program to test above functions */
public static void main(String args[])
{
BinaryTree tree = new BinaryTree();
/* Construct the following binary tree
1
/ \
2 3
/ \
4 5
/
7
*/
tree.root = new Node(1);
tree.root.left = new Node(2);
tree.root.right = new Node(3);
tree.root.left.left = new Node(4);
tree.root.left.right = new Node(5);
tree.root.left.left.left = new Node(7);
tree.printAncestors(tree.root, 7);
}
}
5、在二叉树中查找过定两个节点的最近的公共祖先节点(节点本身可以视为自己的祖先)
方法:
如果o1和o2中的任一个和root匹配,那么root就是最近公共祖先。
如果都不匹配,则分别递归左、右子树。
如果有一个节点出现在左子树,并且另一个节点出现在右子树,则root就是最近公共祖先.
如果两个节点都出现在左子树,则说明最低公共祖先在左子树中,否则在右子树。
继续递归左、右子树,直到遇到step1或者step3的情况。
public int firstLowestCommonAncestor (TreeNode<Integer> root, int o1, int o2) {
if(null == root){
return -1;
}
if(root.val == o1 || root.val == o2){
return root.val;
}
int left = firstLowestCommonAncestor(root.left,o1,o2);
int right = firstLowestCommonAncestor(root.right,o1,o2);
if(left == -1){
return right;
}
if(right == -1){
return left;
}
return root.val;
}
字典树 Trie
字典树,也称为“前缀树”,是一种特殊的树状数据结构,对于解决字符串相关问题非常有效。它能够提供快速检索,主要用于搜索字典中的单词,在搜索引擎中自动提供建议,甚至被用于IP的路由。
- 根节点不包含字符,除根节点外每一个节点都只包含一个字符
- 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串
- 每个节点的所有子节点包含的字符都不相同
以下是在字典树中存储三个单词“top”,“so”和“their”的例子:
这些单词以顶部到底部的方式存储,其中绿色节点“p”,“s”和“r”分别表示“top”,“thus”和“theirs”的底部。
面试中关于字典树的常见问题
计算字典树中的总单词数
打印存储在字典树中的所有单词
使用字典树对数组的元素进行排序
使用字典树从字典中形成单词
构建T9字典(字典树+ DFS )
哈希表 Hashing
哈希法(Hashing)是一个用于唯一标识对象并将每个对象存储在一些预先计算的唯一索引(称为“键(key)”)中的过程。因此,对象以键值对的形式存储,这些键值对的集合被称为“字典”。可以使用键搜索每个对象。基于哈希法有很多不同的数据结构,但最常用的数据结构是哈希表。
哈希表通常使用数组实现
散列数据结构的性能取决于以下三个因素:
哈希函数
哈希表的大小
碰撞处理方法
下图为如何在数组中映射哈希键值对的说明。该数组的索引是通过哈希函数计算的。
面试笔试中关于哈希结构的常见问题:
1、在数组中查找对称键值对
2、追踪遍历的完整路径
3、查找数组是否是另一个数组的子集
4、检查给定的数组是否不相交
图 Graph
图 是一组以网络形式相互连接的节点。节点也称为顶点。 一对节点(x,y)称为边(edge),表示顶点x连接到顶点y。边可以包含权重/成本,显示从顶点x到y所需的成本
图的类型
无向图 有向图
在程序语言中,图可以用两种形式表示:
邻接矩阵 邻接表
常见图遍历算法
广度优先搜索 深度优先搜索
面试笔试中关于图的常见问题
1、实现广度和深度优先搜索
2、检查图是否为树
3、计算图的边数
4、找到两个顶点之间的最短路径