字节面试必须拿下的十道算法题,你会几道?
Posted Big sai
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了字节面试必须拿下的十道算法题,你会几道?相关的知识,希望对你有一定的参考价值。
前言
大家好,我是bigsai。
最近不少小伙伴跟我交流刷题肿么刷,我给的建议就是先剑指offer和力扣hot100,在这些题中还有些重要程度和出现频率是非常非常高的,今天给大家分享当今出现频率最高的10道算法题,学到就是赚到。
0X01翻转链表
力扣206和剑指offer24原题,题意为:
给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
分析:
翻转链表,本意是不创建新的链表节点然后在原链表上实现翻转,但是这个图有点会误导人的思维,其实更好的理解你可以看下面这幅图:
具体实现上两个思路,非递归和递归的实现方式,非递归的实现方式比较简单,利用一个pre节点记录前驱节点,向下枚举的时候改变指针指向就可以,实现代码为:
class Solution
public ListNode reverseList(ListNode head)
if(head==null||head.next==null)//如果节点为NULL或者单个节点直接返回
return head;
ListNode pre=head;//前驱节点
ListNode cur=head.next;//当前节点用来枚举
while (cur!=null)
ListNode next=cur.next;
//改变指向
cur.next=pre;
pre=cur;
cur=next;
head.next=null;//将原先的head节点next置null防止最后成环
return pre;
而递归的方式比较巧妙,借助递归归来的过程巧妙改变指针指向和返回值传递,代码虽然精简但是理解起来有一定难度的,这里用一张图帮助大家理解:
具体代码为:
class Solution
public ListNode reverseList(ListNode head)
if(head==null||head.next==null)//如果最后一个节点不操作
return head;
ListNode node =reverseList(head.next);//先递归 到最底层 然后返回
head.next.next=head;//后面一个节点指向自己
head.next=null;//自己本来指向的next置为null
return node;//返回最后一个节点(一直被递归传递)
0X02设计LRU
对应力扣146LRU缓存机制,题目要求为:
运用你所掌握的数据结构,设计和实现一个 LRU 缓存机制 。实现 LRUCache 类:
LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存
int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
void put(int key, int value) 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
进阶:在 O(1) 时间复杂度内完成这两种操作
详细分析:一次倒在LRU上的经历
LRU的核心就是借助哈希+双链表,哈希用于查询,双链表实现删除只知道当前节点也能O(1)的复杂度删除,不过双链表需要考虑的头尾指针特殊情况。
具体实现的代码为:
class LRUCache
class Node
int key;
int value;
Node pre;
Node next;
public Node()
public Node( int key,int value)
this.key = key;
this.value=value;
class DoubleList
private Node head;// 头节点
private Node tail;// 尾节点
private int length;
public DoubleList()
head = new Node(-1,-1);
tail = head;
length = 0;
void add(Node teamNode)// 默认尾节点插入
tail.next = teamNode;
teamNode.pre=tail;
tail = teamNode;
length++;
void deleteFirst()
if(head.next==null)
return;
if(head.next==tail)//如果删除的那个刚好是tail 注意啦 tail指针前面移动
tail=head;
head.next=head.next.next;
if(head.next!=null)
head.next.pre=head;
length--;
void deleteNode(Node team)
team.pre.next=team.next;
if(team.next!=null)
team.next.pre=team.pre;
if(team==tail)
tail=tail.pre;
team.pre=null;
team.next=null;
length--;
Map<Integer,Node> map=new HashMap<>();
DoubleList doubleList;//存储顺序
int maxSize;
LinkedList<Integer>list2=new LinkedList<>();
public LRUCache(int capacity)
doubleList=new DoubleList();
maxSize=capacity;
public int get(int key)
int val;
if(!map.containsKey(key))
return -1;
val=map.get(key).value;
Node team=map.get(key);
doubleList.deleteNode(team);
doubleList.add(team);
return val;
public void put(int key, int value)
if(map.containsKey(key))// 已经有这个key 不考虑长短直接删除然后更新
Node deleteNode=map.get(key);
doubleList.deleteNode(deleteNode);
else if(doubleList.length==maxSize)//不包含并且长度小于
Node first=doubleList.head.next;
map.remove(first.key);
doubleList.deleteFirst();
Node node=new Node(key,value);
doubleList.add(node);
map.put(key,node);
0X03环形链表
对应力扣141和力扣142,力扣141环形链表要求为:
给定一个链表,判断链表中是否有环,用O(1)内存解决。
详细分析:环形链表找入口,真的太妙了
这个问题利用快慢双指针比较高效,快指针fast每次走2步,slow每次走1步,慢指针走n步到尾时候快指针走了2n步,而环的大小一定小于等于n所以一定会相遇,如果相遇那么说明有环,如果不相遇fast先为null说明无环。
具体代码为:
public class Solution
public boolean hasCycle(ListNode head)
ListNode fast=head;
ListNode slow=fast;
while (fast!=null&&fast.next!=null)
slow=slow.next;
fast=fast.next.next;
if(fast==slow)
return true;
return false;
力扣142是在力扣141拓展,如有有环,返回入环的那个节点,就想下图环形链表返回节点2。
这个问题是需要数学转换的,具体的分析可以看上面的详细分析,这里面提一下大题的步骤。
如果找到第一个交汇点,其中一个停止,另一个继续走,下一次交汇时候刚好走一圈,可以算出循环部分长度为y。
所以我们知道的东西有:交汇时候fast走2x步,slow走x步,环长为y。并且快指针和慢指针交汇时候,多走的步数刚好是换长y的整数倍(它两此刻在同一个位置,快指针刚好多绕整数倍圈数才能在同一个位置相聚),可以得到2x=x+ny(x=ny)。其中所以说慢指针走的x和快指针多走的x是圈长y的整数倍。
也就是说,从开头走到这个点共计x步,从这个点走x步也就是绕了几圈也回到这个点。如果说slow从起点出发,fast从这个点出发(每次走一步,相当于之前两步抵消slow走的路程),那么走x步还会到达这个点,但是这两个指针这次都是每次走一步,所以一旦slow到达循环圈内,两个指针就开始汇合了。
实现代码为:
public class Solution
public ListNode detectCycle(ListNode head)
boolean isloop=false;
ListNode fast=new ListNode(0);//头指针
ListNode slow=fast;
fast.next=head;
if(fast.next==null||fast.next.next==null)
return null;
while (fast!=null&&fast.next!=null)
fast=fast.next.next;
slow=slow.next;
if(fast==slow)
isloop=true;
break;
if(!isloop)//如果没有环返回
return null;
ListNode team=new ListNode(-1);//头指针 下一个才是head
team.next=head;
while (team!=fast) //slow 和fast 分别从起点和当前点出发
team=team.next;
fast=fast.next;
return team;
0X04两个栈实现队列
对应剑指offer09,题意为:
用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )
分析:
解决这个问题,要知道栈是什么,队列是什么,两种常见数据结构格式很简单,栈的特点就是:后进先出,队列的特点就是:先进先出,栈可以想象成一堆书本,越在上面的取的越早,上面来上面出(比喻一下);队列就是想象成排队买东西,只能后面进前面出,所以两者数据结构还是有区别的,虽然都是单个入口进出,但是栈进出口相同,而队列不同。
上面描述的是一个普通栈和队列的数据结构,这里面让我们用两个栈实现一个队列的操作,这里比较容易想的方案就是其中一个栈stack1用作数据存储,插入尾时候直接插入stack1,而删除头的时候将数据先加入到另一个栈stack2中,返回并删除栈顶元素,将stack2顺序加入stack1中实现一个复原,但是这样操作插入时间复杂度为O(1),删除时间复杂度为O(n)比较高。
实现方式也给大家看下:
class CQueue
Stack<Integer>stack1=new Stack<>();
Stack<Integer>stack2=new Stack<>();
public CQueue()
public void appendTail(int value)
stack1.push(value);
public int deleteHead()
if(stack1.isEmpty())
return -1;
while (!stack1.isEmpty())
stack2.push(stack1.pop());
int value= stack2.pop();
while (!stack2.isEmpty())
stack1.push(stack2.pop());
return value;
这样的时间复杂度是不被喜欢的,因为删除太鸡儿耗时了,每次都要折腾一番,有没有什么好的方法能够让删除也方便一点呢?
有啊,stack1可以顺序保证顺序插入,stack1数据放到stack2中可以保证顺序删除,所以用stack1作插入,stack2作删除,因为题目也没要求数据必须放到一个容器中,所以就这样组合使用,完美perfect!
具体实现的时候,插入直接插入到stack1中,如果需要删除从stack2中栈顶删除,如果stack2栈为空那么将stack1中数据全部添加进来(这样又能保证stack2中所有数据是可以顺序删除的了),下面列举几个删除的例子
其实就是将数据分成两个部分,一部分用来插入,一部分用来删除,删除的那个栈stack2空了添加所有stack1中的数据继续操作。这个操作插入删除的时间复杂度是O(1),具体实现的代码为:
class CQueue
Deque<Integer> stack1;
Deque<Integer> stack2;
public CQueue()
stack1 = new LinkedList<Integer>();
stack2 = new LinkedList<Integer>();
public void appendTail(int value)
stack1.push(value);
public int deleteHead()
// 如果第二个栈为空 将stack1数据加入stack2
if (stack2.isEmpty())
while (!stack1.isEmpty())
stack2.push(stack1.pop());
//如果stack2依然为空 说明没有数据
if (stack2.isEmpty())
return -1;
else //否则删除
int deleteItem = stack2.pop();
return deleteItem;
0X05二叉树层序(锯齿)遍历
二叉树的遍历,对应力扣102,107,103.
详细分析:一次面试,被二叉树层序遍历打爆了
如果普通二叉树层序遍历,也不是什么困难的问题,但是它会有个分层返回结果的操作,就需要你详细考虑了。
很多人会用两个容器(队列)进行分层的操作,这里其实可以直接使用一个队列,我们首先记录枚举前队列大小len,然后根据这个大小len去枚举遍历就可以得到完整的该层数据了。
还有一个难点就是二叉树的锯齿层序(也叫之字形打印),第一趟是从左往右,第二趟是从右往左,只需要记录一个奇偶层数进行对应的操作就可以了。
这里就拿力扣103二叉树的锯齿形层序遍历作为题板给大家分享一下代码:
public List<List<Integer>> levelOrder(TreeNode root)
List<List<Integer>> value=new ArrayList<>();//存储到的最终结果
if(root==null)
return value;
int index=0;//判断
Queue<TreeNode>queue=new ArrayDeque<>();
queue.add(root);
while (!queue.isEmpty())
List<Integer>va=new ArrayList<>();//临时 用于存储到value中
int len=queue.size();//当前层节点的数量
for(int i=0;i<len;i++)
TreeNode node=queue.poll();
if(index%2==0)//根据奇偶 选择添加策略
va.add(node.val);
else
va.add(0,node.val);
if(node.left!=null)
queue.add(node.left);
if(node.right!=null)
queue.add(node.right);
value.add(va);
index++;
return value;
0X06 二叉树中后序遍历(非递归)
二叉树的非递归遍历也是考察的重点,对于中序后序遍历递归实现很简单,非递归实现起来还是要点技巧的哦。
详细分析:二叉树的各种遍历(递归、非递归)
对于二叉树的中序遍历,其实就是正常情况第二次访问该节点的时候才抛出输出(第一次数前序),这样我们枚举每个节点第一次不能删除,需要先将它存到栈中,当左子节点处理完成的时候在抛出访问该节点。
核心也就两步,叶子节点左右都为null,也可满足下列条件:
- 枚举当前节点(不存储输出)并用栈存储,节点指向左节点,直到左孩子为null。
- 抛出栈顶访问。如果有右节点,访问其右节点重复步骤1,如有没右节点,继续重复步骤2抛出。
实现代码为:
class Solution
public List<Integer> inorderTraversal(TreeNode root)
List<Integer>value=new ArrayList<Integer>();
Stack<TreeNode> q1 = new Stack();
while(!q1.isEmpty()||root!=null)
while (root!=null)
q1.push(root);
root=root.left;
root=q1.pop();//抛出
value.add(root.val);
root=root.right;//准备访问其右节点
return value;
而后序遍历按照递归的思路其实一般是第三次访问该节点是从右子节点回来才抛出输出,这个实现起来确实有难度。但是具体的实现,我们使用一个pre节点记录上一次被抛出访问的点,如果当前被抛出的右孩子是pre或者当前节点右为null,那么就将这个点抛出,否则说明它的右侧还未被访问需要将它"回炉重造",后面再用!如果不理解可以看前面的详细介绍。
具体实现的代码为:
class Solution
public List<Integer> postorderTraversal(TreeNode root)
TreeNode temp=root;//枚举的临时节点
List<Integer>value=new ArrayList<>();
TreeNode pre=null;//前置节点
Stack<TreeNode>stack=new Stack<>();
while (!stack.isEmpty()||temp!=null)
while(temp!=null)
stack.push(temp);
temp=temp.left;
temp=stack字节面试必须拿下的十道算法题,你会几道?