🔥 面试必备:高频算法题汇总「图文解析 + 教学视频 + 范例代码」必问之 链表 + 栈 + 队列 部分!🔥
链表
链表
是最基本的数据结构,面试官也常常用链表来考察面试者的基本能力,而且链表相关的操作相对而言比较简单,也适合考察写代码的能力。链表的操作也离不开指针,指针又很容易导致出错。
综合多方面的原因,链表题目在面试中占据着很
重要
的地位。
public class ListNode {
int val;
ListNode next;
ListNode(int x) {
val = x;
next = null;
}
}
删除节点
思路:
- 将
下一个节点
复制到当前
public void deleteNode(ListNode node) {
if (node.next == null){
node = null;
return;
}
// 取缔下一节点
node.val = node.next.val
node.next = node.next.next
}
翻转链表
思路
思路:每次都将原第一个结点之后的那个结点放在新的表头后面。
比如1,2,3,4,5
- 第一次:把第一个
结点1
后边的结点2
放到新表头
后面,变成2,1,3,4,5
- 第二次:把第一个
结点1
后边的结点3
放到新表头
后面,变成3,2,1,4,5
- ……
- 直到: 第一个
结点1
,后边没有结点为止。
视频
大圣算法 翻转链表(Reverse Linked List ) -- LeetCode 206
public ListNode reverse(ListNode head) {
//prev表示前继节点
ListNode prev = null;
while (head != null) {
//temp记录下一个节点,head是当前节点
ListNode temp = head.next;
head.next = prev;
prev = head;
head = temp;
}
return prev;
}
中间元素
思路
我总结了一下,可以称为 田忌赛马’
法
public ListNode findMiddle(ListNode head){
if(head == null){
return null;
}
ListNode slow = head;
ListNode fast = head;
// fast.next = null 表示 fast 是链表的尾节点
while(fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
合并两个已排序链表
思路
- 递归方法:首先比较给新链表
接上
一个结点,然后这个结点的next
就是剩下的两条链表合并的结果。
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(0);
ListNode lastNode = dummy;
while (l1 != null && l2 != null) {
if (l1.val < l2.val) {
lastNode.next = l1;
l1 = l1.next;
} else {
lastNode.next = l2;
l2 = l2.next;
}
lastNode = lastNode.next;
}
if (l1 != null) {
lastNode.next = l1;
} else {
lastNode.next = l2;
}
return dummy.next;
}
链表排序
归并排序
-
归并排序
的也是基于分治
的思想,但是与快排不同的是归并是先划分
,然后从底层开始向上合并
。 -
归并排序的主要思想是将两个
已经排好序的分段
合并成一个有序的分段
。除了找到中间节点的操作必须遍历链表外,其它操作与数组的归并排序基本相同。
视频
public ListNode sortList(ListNode head) {
if (head == null || head.next == null) {
return head;
}
// 取得中间节点,将链表一分为二
ListNode mid = findMiddle(head);
ListNode right = sortList(mid.next);
mid.next = null;
ListNode left = sortList(head);
return mergeTwoLists(left, right);
}
// 查找中间元素算法
public ListNode findMiddle(ListNode head){
if(head == null){
return null;
}
ListNode slow = head;
ListNode fast = head;
// fast.next = null 表示 fast 是链表的尾节点
while(fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
// 合并两个有序链表
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(0);
ListNode lastNode = dummy;
while (l1 != null && l2 != null) {
if (l1.val < l2.val) {
lastNode.next = l1;
l1 = l1.next;
} else {
lastNode.next = l2;
l2 = l2.next;
}
lastNode = lastNode.next;
}
if (l1 != null) {
lastNode.next = l1;
} else {
lastNode.next = l2;
}
return dummy.next;
}
快速排序
快速排序的主要思想是:
-
选定一个
基准元素
-
经过一趟排序,将所有元素分成
两部分
-
分别对两部分重复上述操作,直到所有元素都已排序成功
因为单链表只能从链表头节点向后遍历,没有prev
指针,因此必须选择头节点作为基准元素。这样第二步操作的时间复杂度就为O(n)
。由于之后都是分别对两部分完成上述操作,因此会将链表划分为lgn个段,因此时间复杂度为O(nlgn)
public ListNode sortList(ListNode head) {
quickSort(head, null);
return head;
}
private void quickSort(ListNode start, ListNode end) {
if (start == end) {
return;
}
ListNode pt = partition(start, end);
quickSort(start, pt);
quickSort(pt.next, end);
}
// 快排 轮状法
private ListNode partition(ListNode start, ListNode end) {
int pivotKey = start.val;
ListNode p1 = start, p2 = start.next;
while (p2 != end) {
if (p2.val < pivotKey) {
p1 = p1.next;
swapValue(p1, p2);
}
p2 = p2.next;
}
swapValue(start, p1);
return p1;
}
private void swapValue(ListNode node1, ListNode node2) {
int tmp = node1.val;
node1.val = node2.val;
node2.val = tmp;
}
两个链表是否相交
思路
-
如果两个单链表有
共同
的节点 -
那么从第一个节点开始,后面的节点都会
重叠
,直至链表结束 -
因为两个链表中有一个
共同
节点 -
则从这个节点里的
指针域
指向下一个节点的地址就相同 -
所以
相交以后
的节点就会相同
,直至链表结束,总的模型就像一个“Y”
视频
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null) {
return null;
}
ListNode currA = headA;
ListNode currB = headB;
int lengthA = 0;
int lengthB = 0;
// 让长的先走到剩余长度和短的一样
while (currA != null) {
currA = currA.next;
lengthA++;
}
while (currB != null) {
currB = currB.next;
lengthB++;
}
currA = headA;
currB = headB;
while (lengthA > lengthB) {
currA = currA.next;
lengthA--;
}
while (lengthB > lengthA) {
currB = currB.next;
lengthB--;
}
// 然后同时走到第一个相同的地方
while (currA != currB) {
currA = currA.next;
currB = currB.next;
}
// 返回交叉开始的节点
return currA;
}
栈 / 队列
- 栈(stack)又名堆栈:
它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。
- 队列是一种特殊的线性表
特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。
带最小值操作的栈
这道面试题主要考察我们对于辅助栈的使用。
常见的辅助栈包括两种:
-
辅助栈和数据栈同步
-
辅助栈和数据栈不同步
我们这里采用辅助栈和数据栈同步的方式:
特点:编码简单,不用考虑一些边界情况,就有一点不好:辅助栈可能会存一些“不必要”的元素。
-
辅助栈为空的时候,必须放入新进来的数;
-
新来的数小于或者等于辅助栈栈顶元素的时候,才放入,特别注意这里“等于”要考虑进去,因为出栈的时候,连续的、相等的并且是最小值的元素要同步出栈;
-
出栈的时候,辅助栈的栈顶元素等于数据栈的栈顶元素,才出栈。
总结一下:出栈时,最小值出栈才同步;入栈时,最小值入栈才同步。
public class MinStack {
private Stack<Integer> stack;
private Stack<Integer> minStack; // 维护一个辅助栈,传入当前栈的最小值
public MinStack() {
stack = new Stack<Integer>();
minStack = new Stack<Integer>();
}
public void push(int number) {
stack.push(number);
if (minStack.isEmpty()) {
minStack.push(number);
} else {
minStack.push(Math.min(number, minStack.peek()));
}
}
public int pop() {
minStack.pop();
return stack.pop();
}
public int min() {
return minStack.peek();
}
}
有效括号
思路:
-
初始化栈 S。
-
一次处理表达式的每个括号。
-
如果遇到开括号,我们只需将其推到栈上即可。这意味着我们将稍后处4理它,让我们简单地转到前面的 子表达式。
-
如果我们遇到一个闭括号,那么我们检查栈顶的元素。如果栈顶的元素是一个 相同类型的 左括号,那么我们将它从栈中弹出并继续处理。否则,这意味着表达式无效。
-
如果到最后我们剩下的栈中仍然有元素,那么这意味着表达式无效。
视频
public boolean isValidParentheses(String s) {
Stack<Character> stack = new Stack<Character>();
for (Character c : s.toCharArray()) {
if ("({[".contains(String.valueOf(c))) {
stack.push(c);
} else {
if (!stack.isEmpty() && isValid(stack.peek(), c)) {
stack.pop();
} else {
return false;
}
}
}
return stack.isEmpty();
}
private boolean isValid(char c1, char c2) {
return (c1 == '(' && c2 == ')') || (c1 == '{' && c2 == '}')
|| (c1 == '[' && c2 == ']');
}
用栈实现队列
思路:
-
思路是有两个栈,一个用来放数据(数据栈),一个用来辅助(辅助栈)。
-
数据添加时,会依次压人栈,取数据时肯定会取栈顶元素,但我们想模拟队列的先进先出,所以就得取栈底元素,那么辅助栈就派上用场了
-
把数据栈的元素依次弹出到辅助栈,但保留最后一个元素,最后数据栈就剩下了最后一个元素,直接把元素返回,这时数据栈已经没有了数据。
-
最后呢,把辅助栈的元素依次压人数据栈,这样,我们成功取到了栈底元素。
视频
public class MyQueue {
private Stack<Integer> outStack;
private Stack<Integer> inStack;
public MyQueue() {
outStack = new Stack<Integer>();
inStack = new Stack<Integer>();
}
private void in2OutStack(){
while(!inStack.isEmpty()){
outStack.push(inStack.pop());
}
}
public void push(int element) {
inStack.push(element);
}
public int pop() {
if(outStack.isEmpty()){
this.in2OutStack();
}
return outStack.pop();
}
public int top() {
if(outStack.isEmpty()){
this.in2OutStack();
}
return outStack.peek();
}
}
逆波兰表达式求值 (后缀)
思路:
-
逆波兰表达式求解,定义一个栈辅助计算
-
当遇到运算符"+"、"-"、"*"、"/"时,从栈中pop出两个数字计算,否则将数字入栈;
public int evalRPN(String[] tokens) {
Stack<Integer> s = new Stack<Integer>();
String operators = "+-*/";
for (String token : tokens) {
if (!operators.contains(token)) {
s.push(Integer.valueOf(token));
continue;
}
// 这里有个坑
int a = s.pop();
int b = s.pop();
// 先出的在运算符后
// 后出的在运算符前
if (token.equals("+")) {
s.push(b + a);
} else if(token.equals("-")) {
s.push(b - a);
} else if(token.equals("*")) {
s.push(b * a);
} else {
s.push(b / a);
}
}
return s.pop();
}
Attention
为了提高文章质量,防止冗长乏味
下一部分算法题
-
本片文章篇幅总结越长。我一直觉得,一片过长的文章,就像一场超长的 会议/课堂,体验很不好,所以打算再开一篇文章来总结其余的考点
-
在后续文章中,我将继续针对
链表
栈
队列
堆
动态规划
矩阵
位运算
等近百种,面试高频算法题,及其图文解析 + 教学视频 + 范例代码
,进行深入剖析有兴趣可以继续关注 _yuanhao 的编程世界
相关文章
🔥面试必备:高频算法题汇总「图文解析 + 教学视频 + 范例代码」必知必会 排序 + 二叉树 部分!🔥
每个人都要学的图片压缩终极奥义,有效解决 Android 程序 OOM
Android 让你的 Room 搭上 RxJava 的顺风车 从重复的代码中解脱出来
ViewModel 和 ViewModelProvider.Factory:ViewModel 的创建者
单例模式-全局可用的 context 对象,这一篇就够了
缩放手势 ScaleGestureDetector 源码解析,这一篇就够了
Android 属性动画框架 ObjectAnimator、ValueAnimator ,这一篇就够了
看完这篇再不会 View 的动画框架,我跪搓衣板
看完这篇还不会 GestureDetector 手势检测,我跪搓衣板!
android 自定义控件之-绘制钟表盘
欢迎关注_yuanhao的博客园!
为了方便大家跟进学习,我在 GitHub 建立了一个仓库
仓库地址:超级干货!精心归纳视频、归类、总结
,各位路过的老铁支持一下!给个 Star !