继续分享面试经常出现的算法题和解法。
第一部分见:字节面试高频百题(一)
第二部分见:字节面试高频百题(二)
41、判断回文
牛客 141 判断是否为回文字符串:
思路:
- 前后指针相向而行
go
public boolean judge (String s) {
// 双指针算法,判断回文串
// 一左一右两个指针相向而行
int left = 0, right = s.length() - 1;
while (left < right) {
if (s.charAt(left) != s.charAt(right)) {
return false;
}
left++;
right--;
}
return true;
}
42、单链表的排序
牛客70 单链表的排序:
思路:
- 快慢指针找中点,拆成两个链表,合并
go
//返回两个排好序且合并好的子链表
public ListNode sortInList (ListNode head) {
//终止条件:链表为空或者只有一个元素,直接就是有序的
if (head == null || head.next == null) return head;
ListNode left = head;
ListNode mid = head.next;
ListNode right = head.next.next;
//本级任务:找到这个链表的中间节点,从前面断开,分为左右两个子链表,进入子问题排序
//右边的指针到达末尾时,中间的指针指向该段链表中间
while (right != null && right.next != null) {
left = left.next;
mid = mid.next;
right = right.next.next;
}
//左边指针指向左端的最右一个节点,从这里断开
left.next = null;
//拆解左右链表分别排序
ListNode leftHead = sortInList(head);
ListNode rightHead = sortInList(mid);
//合并两个升序链表
return mergeTwoLists(leftHead, rightHead );
}
//合并两个有序链表的函数
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
// 虚拟头节点
ListNode dummy = new ListNode(-1), p = dummy;
ListNode p1 = list1, p2 = list2;
while (p1 != null && p2 != null) {
// 比较 p1 和 p2 两个指针,将值较小的节点接到 p 指针
if (p1.val > p2.val) {
p.next = p2;
p2 = p2.next;
} else {
p.next = p1;
p1 = p1.next;
}
// p 指针不断前进
p = p.next;
}
// p2 走完,把 p1 剩下的节点接到 p 上
if (p1 != null) {
p.next = p1;
}
// p1 走完,把 p2 剩下的节点接到 p 上
if (p2 != null) {
p.next = p2;
}
// 返回去掉虚拟头节点的链表
return dummy.next;
}
43、数组中出现次数超过一半的数字
牛客 73 数组中出现次数超过一半的数字:
思路:
-
排序取中位数
-
哈希表存数值到个数的映射
-
投票法。假设某个数是众数,则票数+1,遇到非嫌疑众数的数,票数-1,当票数为0则说明当前数不是众数
go
public int MoreThanHalfNum_Solution (int[] numbers) {
Arrays.sort(numbers);
return numbers[numbers.length / 2];
}
go
HashMap<Integer,Integer> num2Cnt = new HashMap<>();
public int MoreThanHalfNum_Solution (int[] numbers) {
int half = numbers.length / 2;
for(int i = 0;i < numbers.length ;i++){
int curNumCnt = num2Cnt.getOrDefault(numbers[i],0);
num2Cnt.put(numbers[i],curNumCnt+1);
if(curNumCnt + 1 > half) return numbers[i];
}
return -1;
}
go
public int MoreThanHalfNum_Solution (int[] numbers) {
//初始化候选人为第一个元素,候选人的投票次数为1
int cond =numbers[0] , cnt = 1;
for (int i = 1; i < numbers.length; i++) {
//如果当前数=cond,则cnt++,否则cnt--
if (cond == numbers[i]) cnt++;
else cnt--;
//cnt为0,选择下一个元素为候选元素,并且置count=1
if (cnt == 0) {
cond = numbers[i+1];
//更新投票次数
cnt = 1;
}
}
return cond;
}
44、平衡二叉树
力扣110/牛客 62 判断是否为平衡二叉树:
-
https://leetcode.cn/problems/balanced-binary-tree/description/
-
https://www.nowcoder.com/practice/8b3b95850edb4115918ecebdf1b4d222?tpId=117
思路:
- dfs,判断左子树为平衡树,判断右子树为平衡树,左子树深度和右子树深度差距小于等于1为平衡树
O(n)解法:
go
public boolean IsBalanced_Solution (TreeNode root) {
if (root == null)return true;
//深度差
return getDepthDiff(root) != -1;
}
//返回root为根节点对应的树的左右子树的深度差
public int getDepthDiff(TreeNode root) {
if (root == null) return 0;
//递归计算当前root左子树的深度差
int left = getDepthDiff(root.left);
//当前节点左子树不平衡,则该树不平衡,-1表达深度大于1的场景
if (left < 0) return -1;
int right = getDepthDiff(root.right);
if (right < 0) return -1;
//计算深度差:大于1认为是 -1,否则取最大+1(把根节点弄进来)
return Math.abs(left - right) > 1 ? -1 : Math.max(left, right) + 1;
}
O(n2)解法:
go
public boolean isBalanced(TreeNode root) {
if (root == null)
return true;
if (!isBalanced(root.left) || !isBalanced(root.right))
return false;
return Math.abs(depth(root.left) - depth(root.right)) <= 1;
}
// 返回root为根节点对应的树的高度
public int depth(TreeNode root) {
if (root == null)
return 0;
return Math.max(depth(root.left), depth(root.right)) + 1;
}
45、矩阵的最小路径和
力扣 64/牛客 59 矩阵最小路径和:
思路:
-
动态规划:dp[i][j] = dp[i][j] + Min(matrix[i][j-1],matrix[i-1][j])
-
dfs,要求空间复杂度为O(n),则说明不能再新建 dp 矩阵,直接使用matrix
go
public int minPathSum (int[][] matrix) {
int m = matrix.length, n = matrix[0].length;
dp[i][j] = dp[i][j] + Min(matrix[i][j-1],matrix[i-1][j])
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (i == 0 && j == 0) continue;
if (i == 0 ) {
matrix[i][j] = matrix[i][j] + matrix[i][j - 1];
} else if (j == 0) {
matrix[i][j] = matrix[i][j] + matrix[i - 1][j];
} else {
matrix[i][j] = matrix[i][j] + Math.min(matrix[i][j - 1], matrix[i - 1][j]);
}
}
}
return matrix[m - 1][n - 1];
}
46、表达式求值
牛客 137 表达式求值:
思路:
- 栈+递归
go
public int solve (String s) {
ArrayList<Integer> res = function(s, 0);
return res.get(0);
}
public ArrayList<Integer> function(String s, int index) {
Stack<Integer> stack = new Stack<Integer>();
int num = 0;
char op = '+';
int i;
for (i = index; i < s.length(); i++) {
//数字转换成int数字
//判断是否为数字
if (s.charAt(i) >= '0' && s.charAt(i) <= '9') {
num = num * 10 + s.charAt(i) - '0';
if (i != s.length() - 1)
continue;
}
//碰到'('时,把整个括号内的当成一个数字处理
if (s.charAt(i) == '(') {
//递归处理括号
ArrayList<Integer> res = function(s, i + 1);
num = res.get(0);
i = res.get(1);
if (i != s.length() - 1)
continue;
}
switch (op) {
//加减号先入栈
case '+':
stack.push(num);
break;
case '-':
//相反数
stack.push(-num);
break;
//优先计算乘号
case '*':
int temp = stack.pop();
stack.push(temp * num);
break;
}
num = 0;
//右括号结束递归
if (s.charAt(i) == ')')
break;
else
op = s.charAt(i);
}
int sum = 0;
//栈中元素相加
while (!stack.isEmpty())
sum += stack.pop();
ArrayList<Integer> temp = new ArrayList<Integer>();
temp.add(sum);
temp.add(i);
return temp;
}
47、逆波兰表达式求值
牛客 216 逆波兰表达式求值:
思路:
- 栈。遇到数字入栈,遇见符号弹出两个数字做运算,然后将结果入栈
go
public int evalRPN (String[] tokens) {
Stack<Integer> stack = new Stack<>();
for (int i = 0 ; i < tokens.length; ++i) {
if (isOperator(tokens[i])) {
Integer num1 = stack.pop();
Integer num2 = stack.pop();
Integer res = operator(num2, num1, tokens[i]);
stack.push(res);
} else {
stack.push(Integer.parseInt(tokens[i]));
}
}
return stack.pop();
}
static int operator(int n1, int n2, String op) {
if (op.equals("+")) return n1 + n2;
else if (op.equals("-")) return n1 - n2;
else if (op.equals("*")) return n1 * n2;
else if (op.equals("/")) return n1 / n2;
else return -1;
}
static boolean isOperator(String op) {
return op.equals("+") || op.equals("-") || op.equals("*") || op.equals("/");
}
48、最小的K个数
牛客 119 最小的K个数:
思路:
- topk 问题求最小,建大顶堆
自建大顶堆的代码:
go
public ArrayList<Integer> GetLeastNumbers_Solution (int[] input, int k) {
ArrayList<Integer> res = new ArrayList<>();
if (input.length == 0 || k == 0) return res;
//topk 求最小,建大顶堆
pq = new int[k + 1];
//构建前k个元素组成的大顶堆
for (int i = 0; i < k; i++) {
insert(input[i]);
}
//从第k+1个开始依次比较堆顶元素,比堆顶元素小则替换
for (int i = k; i < input.length; i++) {
int maxVal = pq[1];
if (input[i] < maxVal) {
pq[1] = input[i];
//让pq[1]下沉到正确位置
sink(1);
}
}
for (int i = 1; i <= k; i++) {
res.add(pq[i]);
}
return res;
}
//存储元素的数组
private int[] pq;
// 当前元素个数
private int size = 0;
//下沉第 x 个元素,以维护最大堆性质
private void sink(int x) {
// 如果沉到堆底,就沉不下去了
while (left(x) <= size) {
// 先假设左边节点较大
int max = left(x);
// 如果右边节点存在,比一下大小
if (right(x) <= size && less(max, right(x)))
max = right(x);
// 结点 x 比俩孩子都大,就不必下沉了
if (less(max, x)) break;
// 否则,不符合最大堆的结构,下沉 x 结点
swap(x, max);
x = max;
}
}
private int parent(int root) {
return root / 2;
}
private int left(int root) {
return root * 2;
}
private int right(int root) {
return root * 2 + 1;
}
//插入函数
private void insert(int val) {
size++;
//把新元素加到最后
pq[size] = val;
//让它上浮到正确的位置
swim((size));
}
//上浮第 x 个元素,以维护最大堆性质
private void swim(int x) {
//如果浮到堆顶,就不能再上浮
while (x > 1 && less(parent(x), x)) {
//如果第x个元素比上层大
//将x换上去
swap(parent(x), x);
x = parent(x);
}
}
//pq[i]是否比pq[j]小
private boolean less(int i, int j) {
return pq[i] - pq[j] < 0;
}
//交换数组的两个元素
private void swap(int i, int j) {
int temp = pq[i];
pq[i] = pq[j];
pq[j] = temp;
}
使用JDK优先级队列的代码:
go
public ArrayList<Integer> GetLeastNumbers_Solution (int[] input, int k) {
ArrayList<Integer> res = new ArrayList<>();
if (input.length == 0 || k == 0) return res;
//topk 求最小,建大顶堆
PriorityQueue<Integer> pq = new PriorityQueue<>(k,(a,b)->(b-a));
//构建前k个元素组成的大顶堆
for (int i = 0; i < k; i++) {
pq.offer(input[i]);
}
//从第k+1个开始依次比较堆顶元素,比堆顶元素小则替换
for (int i = k; i < input.length; i++) {
int maxVal = pq.peek();
if (input[i] < maxVal) {
pq.poll();
pq.offer(input[i]);
}
}
while(!pq.isEmpty()){
res.add(pq.poll());
}
return res;
}
49、字符串出现次数的TopK问题
牛客97 字符串出现次数的TopK问题:
思路:
- 最大堆实现的优先级队列
go
public String[][] topKstrings (String[] strings, int k) {
// write code here
PriorityQueue<MyNode> queue = new PriorityQueue<>(new MyComparator());
HashMap<String, Integer> map = new HashMap<>();
for (int i = 0; i < strings.length; i++) {
map.put(strings[i], map.getOrDefault(strings[i], 0) + 1);
}
//入堆
for (Map.Entry<String, Integer> entry : map.entrySet()) {
queue.add(new MyNode(entry.getKey(), entry.getValue()));
}
String[][] result = new String[k][2];
int j = 0;
while (j < k && !queue.isEmpty()) {
MyNode node = queue.poll();
result[j][0] = node.val;
result[j++][1] = String.valueOf(node.num);
}
return result;
}
class MyNode {
String val;
int num;
MyNode(String val, int num) {
this.num = num;
this.val = val;
}
}
class MyComparator implements Comparator<MyNode> {
@Override
public int compare(MyNode o1, MyNode o2) {
if (o1.num == o2.num) {
//字典序小的在前 所以 o1 比 o2
return o1.val.compareTo(o2.val);
} else {
//数量大的在前所以 o2 - o1
return o2.num - o1.num;
}
}
}
50、进制转换
牛客 112 进制转换:
思路:
- 除N取余,然后倒序排列,高位补零。
go
public String solve (int M, int N) {
if (M == 0) return "0";
String s = "0123456789ABCDEF";
StringBuffer sb = new StringBuffer();
boolean f = false;
if (M < 0) {
f = true;
M = -M;
}
while (M != 0) {
sb.append(s.charAt(M % N));
M /= N;
}
if (f) sb.append("-");
return sb.reverse().toString();
}
51、判断一个链表是否为回文结构
牛客 96 判断一个链表是否为回文结构
思路:
- 找到中间节点,反转后半部分,两部分从头等值比对
go
public boolean isPail (ListNode head) {
//快慢指针找到中点
ListNode fast = head, slow = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
}
//fast != null 说明是奇数长度
if (fast != null) slow = slow = slow.next;
slow = reverseList(slow);
fast = head;
while (slow != null && fast != null) {
if (slow.val != fast.val) return false;
fast = fast.next;
slow = slow.next;
}
return true;
}
public ListNode reverseList(ListNode head) {
if (head == null || head.next == null) return head;
ListNode last = reverseList(head.next);
head.next.next = head;
head.next = null;
return last;
}
52、二叉树中和为某一值的路径(是否存在)
牛客 9 二叉树中和为某一值的路径(一):
思路:
- 前序遍历,每走过一个节点更新 sum 值,sum 为0 且是叶子节点更新路径和标识,并退出
go
//路径和等于sum的存在标识
private boolean has = false;
public boolean hasPathSum (TreeNode root, int sum) {
//遍历二叉树,走过每个节点更新sum值,sum为0且是叶子节点更新路径和标识,并退出
traverse(root, sum);
return has;
}
private void traverse(TreeNode root, int sum) {
if (root == null) return;
sum = sum - root.val;
//sum为0并且当前节点是叶子节点
if (sum == 0 && root.left == null && root.right == null) {
has = true;
return;
}
traverse(root.left, sum);
traverse(root.right, sum);
}
53、二叉树中和为某一值的路径(所有路径)
牛客 8 二叉树中和为某一值的路径(二)::
思路:
- 回溯的思路,在(一)的遍历基础上增加路径更新、结果追加、撤销选择的逻辑。
go
ArrayList<ArrayList<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public ArrayList<ArrayList<Integer>> FindPath (TreeNode root, int sum) {
traverse(root, sum);
return res;
}
private void traverse(TreeNode root, int sum) {
if (root == null) return;
//路径更新
path.add(root.val);
sum = sum - root.val;
//sum为0并且当前节点是叶子节点
if (sum == 0 && root.left == null && root.right == null) {
//找到一条路径
res.add(new ArrayList(path));
}
traverse(root.left, sum);
traverse(root.right, sum);
//撤销选择
path.removeLast();
}
54、二叉树中和为某一值的路径(所有子路径)
牛客 162 二叉树中和为某一值的路径(三):
思路:
-
在(二)的基础上,再次进行树的遍历统计路径数
-
注意:路径不再要求叶子节点结束
go
private int res = 0;
public int FindPath (TreeNode root, int sum) {
if (root == null) return res;
//查询根节点的路径数
traverse(root,sum);
//查询子节点的路径数
FindPath(root.left,sum);
FindPath(root.right,sum);
return res;
}
//查询以某节点为根的路径数
private void traverse(TreeNode root, int sum) {
if (root == null) return;
sum = sum - root.val;
//sum为0【注意此处不再要求叶子节点结束,要去掉叶子节点判断】
if (sum == 0) {
res++;
}
traverse(root.left, sum);
traverse(root.right, sum);
}
55、链表内指定区间反转
力扣 92 反转链表2:
牛客21 链表内指定区间反转:
go
public ListNode reverseBetween(ListNode head, int left, int right) {
// base case
if (left == 1)
return reverseN(head, right);
// 前进到反转的起点触发 base casee
head.next = reverseBetween(head.next, left - 1, right - 1);
return head;
}
// 后驱节点
ListNode successor = null;
// 反转以 head 为起点的 n 个节点,返回新的头节点
ListNode reverseN(ListNode head, int n) {
if (n == 1) {
// 记录第 n+1 个节点
successor = head.next;
return head;
}
// 以 head.next 为起点,需要反转前 n-1 个节点
ListNode last = reverseN(head.next, n - 1);
// head.next为反转后的表尾,表尾追加head
head.next.next = head;
// 让反转之后的 head 节点和后面的节点连起来
head.next = successor;
return last;
}
56、不同路径的数目
力扣 63/牛客 34 不同路径的数目:
思路:
- dp 迭代 or 递归
go
public int uniquePaths (int m, int n) {
//dp[i][j]代表 [0][0]到[i][j]的路径数
int[][] dp = new int[m][n];
for(int i = 0;i < m;i++){
for(int j = 0;j <n;j++){
if(i == 0 || j == 0) {
dp[i][j] = 1;
continue;
}
dp[i][j] = dp[i][j-1] + dp[i-1][j];
}
}
return dp[m-1][n-1];
}
go
public int uniquePaths (int m, int n) {
if (m == 1 || n == 1) return 1;
return uniquePaths(m - 1, n) + uniquePaths(m, n - 1);
}
57、合并区间
力扣 56/牛客 37 合并区间:
思路:
-
对于几个相交区间合并后的结果区间 x,x.start 一定是这些相交区间中 start 最小的,x.end 一定是这些相交区间中 end 最大的
-
对 start 排序
go
/*
* public class Interval {
* int start;
* int end;
* public Interval(int start, int end) {
* this.start = start;
* this.end = end;
* }
* }
*/
//对于几个相交区间合并后的结果区间 x,x.start 一定是这些相交区间中 start 最小的,x.end 一定是这些相交区间中 end 最大的
public ArrayList<Interval> merge (ArrayList<Interval> intervals) {
LinkedList<Interval> res = new LinkedList<>();
if(intervals.isEmpty()) return new ArrayList(res);
intervals.sort((a,b)->(a.start-b.start));
Interval firstInterval = intervals.get(0);
res.add(firstInterval);
for(int i = 1; i < intervals.size();i++){
Interval curr = intervals.get(i);
//res 中最后一个元素的引用
Interval last = res.getLast();
if(curr.start <= last.end){
last.end = Math.max(last.end,curr.end);
}else{
//处理下一个待合并区间
res.add(curr);
}
}
return new ArrayList(res);
}
58、排序数组中找到上中位数
牛客 36 在两个长度相等的排序数组中找到上中位数:
思路:
- 双指针
go
public int findMedianinTwoSortedAray (int[] arr1, int[] arr2) {
int len = arr1.length + arr2.length;
int mid = 0;
//求出合并后中位数的下标
if (len % 2 == 0) mid = len / 2;
else mid = len / 2 + 1;
int index1= 0,index2 = 0;
int res = 0;
for(int i = 0;i < mid;i++){
if(arr1[index1] < arr2[index2]){
res = arr1[index1];
index1++;
}else{
res = arr2[index2];
index2++;
}
}
return res;
}
59、搜索二叉树和完全二叉树
牛客 60 判断一棵二叉树是否为搜索二叉树和完全二叉树
思路:
-
搜索二叉树,明确定义是 root为左子树的最大值,右子树的最小值,遍历参数引入双节点
-
完全二叉树,每一层不存在空节点。层序遍历,null值也入队。遍历遇到空节点,检查队列是否还有非空节点,存在说明二叉树不完全。
go
public boolean[] judgeIt (TreeNode root) {
boolean isBst = traverse(root, null, null);
return new boolean[] {isBst, isAllTree(root)};
}
public boolean traverse(TreeNode root, TreeNode min, TreeNode max) {
if (root == null) return true;
if (min != null && min.val > root.val) return false;
if (max != null && max.val < root.val) return false;
//root 为左子树的最大值
boolean leftIsBst = traverse(root.left, min, root);
//root 为右子树的最小值
boolean rightIsBst = traverse(root.right, root, max);
return leftIsBst && rightIsBst;
}
public boolean isAllTree(TreeNode root) {
if (root == null) return true;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
//遍历到的节点出队
TreeNode node = queue.poll();
if(node != null){
//【此处不判空】子节点是空值也入队
queue.offer(node.left);
queue.offer(node.right);
}else{
//遇到空节点,检查队列是否还有非空节点,存在说明二叉树不完全
while (!queue.isEmpty()) {
if(queue.poll() != null) return false;
}
}
}
return true;
}
60、删除有序链表中重复的元素-II
牛客 24 删除有序链表中重复的元素-II
思路:
- 双指针,隔一齐头并进,出现重复则只前进快指针,直到不相等的时候执行删除操作
go
public ListNode deleteDuplicates (ListNode head) {
if (head == null) return null;
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode slow = head, fast = head.next;
ListNode pre = dummy;
while (fast != null) {
//两值不等,齐头并进
if (slow.val != fast.val) {
//如果距离相差1,更新前驱指针,继续向前
if (slow.next == fast) {
pre = slow;
slow = slow.next;
fast = fast.next;
}else{
//执行删除操作 fast 此时在最后一个删除节点后,slow此时是待删除的第一个节点
pre.next = fast;
slow = pre;
}
} else if (slow.val == fast.val) {
//处理结尾重复
if(fast.next == null){
pre.next = null;
}
//值相等后,slow不动,fast继续走
fast = fast.next;
}
}
return dummy.next;
}
我是蜗牛,大厂程序员,专注技术原创和个人成长,正在互联网上摸爬滚打。欢迎关注我,和蜗牛一起成长,我们一起牛~下期见!
推荐阅读:
点阅读原文发现更多Java资源宝藏~
go
点分享
点收藏
点点赞
点在看