Attention

秋招接近尾声,我总结了 牛客WanAndroid 上,有关笔试面经的帖子中出现的算法题,结合往年考题写了这一系列文章,所有文章均与 LeetCode 进行核对、测试。欢迎食用


本文将覆盖 「二进制」 + 「位运算」 和 Lru 方面的面试算法题,文中我将给出:

  1. 面试中的题目
  2. 解题的思路
  3. 特定问题的技巧和注意事项
  4. 考察的知识点及其概念
  5. 详细的代码和解析

开始之前,我们先看下会有哪些重点案例:

为了方便大家跟进学习,我在 GitHub 建立了一个仓库

仓库地址:超级干货!精心归纳视频、归类、总结,各位路过的老铁支持一下!给个 Star !

现在就让我们开始吧!


矩阵


螺旋矩阵


给定一个包含 m x n 个要素的矩阵,(m 行, n 列),按照螺旋顺序,返回该矩阵中的所有要素。

示例 :

  1. 输入:
  2. [
  3. [1, 2, 3, 4],
  4. [5, 6, 7, 8],
  5. [9,10,11,12]
  6. ]
  7. 输出: [1,2,3,4,8,12,11,10,9,5,6,7]

解题思路

我们定义矩阵的第 k 层是到最近边界距离为 k 的所有顶点。例如,下图矩阵最外层元素都是第 1 层,次外层元素都是第 2 层,然后是第 3 层的。

  1. [[1, 1, 1, 1, 1, 1, 1],
  2. [1, 2, 2, 2, 2, 2, 1],
  3. [1, 2, 3, 3, 3, 2, 1],
  4. [1, 2, 2, 2, 2, 2, 1],
  5. [1, 1, 1, 1, 1, 1, 1]]

对于每层,我们从左上方开始以顺时针的顺序遍历所有元素,假设当前层左上角坐标是 \(\text{(r1, c1)}\),右下角坐标是 \(\text{(r2, c2)}\)。

首先,遍历上方的所有元素 (r1, c),按照 c = c1,...,c2 的顺序。然后遍历右侧的所有元素 (r, c2),按照 r = r1+1,...,r2 的顺序。如果这一层有四条边(也就是 r1 < r2 并且 c1 < c2 ),我们以下图所示的方式遍历下方的元素和左侧的元素。

  1. public List<Integer> spiralOrder(int[][] matrix) {
  2. ArrayList<Integer> rst = new ArrayList<Integer>();
  3. if(matrix == null || matrix.length == 0) {
  4. return rst;
  5. }
  6. int rows = matrix.length;
  7. int cols = matrix[0].length;
  8. int count = 0;
  9. while(count * 2 < rows && count * 2 < cols){
  10. for (int i = count; i < cols - count; i++) {
  11. rst.add(matrix[count][i]);
  12. }
  13. for (int i = count + 1; i < rows - count; i++) {
  14. rst.add(matrix[i][cols - count - 1]);
  15. }
  16. if (rows - 2 * count == 1 || cols - 2 * count == 1) { // 如果只剩1行或1列
  17. break;
  18. }
  19. for (int i = cols - count - 2; i >= count; i--) {
  20. rst.add(matrix[rows - count - 1][i]);
  21. }
  22. for (int i = rows - count - 2; i >= count + 1; i--) {
  23. rst.add(matrix[i][count]);
  24. }
  25. count++;
  26. }
  27. return rst;
  28. }

判断数独是否合法


请判定一个数独是否有效。该数独可能只填充了部分数字,其中缺少的数字用 . 表示。

维护一个HashSet用来记同一、同一、同一九宫格是否存在相同数字

示例 :

  1. 输入:
  2. [
  3. ["8","3",".",".","7",".",".",".","."],
  4. ["6",".",".","1","9","5",".",".","."],
  5. [".","9","8",".",".",".",".","6","."],
  6. ["8",".",".",".","6",".",".",".","3"],
  7. ["4",".",".","8",".","3",".",".","1"],
  8. ["7",".",".",".","2",".",".",".","6"],
  9. [".","6",".",".",".",".","2","8","."],
  10. [".",".",".","4","1","9",".",".","5"],
  11. [".",".",".",".","8",".",".","7","9"]
  12. ]
  13. 输出: false
  14. 解释: 除了第一行的第一个数字从 5 改为 8 以外,空格内其他数字均与 示例1 相同。
  15. 但由于位于左上角的 3x3 宫内有两个 8 存在, 因此这个数独是无效的。

说明:

一个有效的数独(部分已被填充)不一定是可解的。

只需要根据以上规则,验证已经填入的数字是否有效即可

给定数独序列只包含数字 1-9 和字符 '.'

给定数独永远是 9x9 形式的。`

解题思路

一次迭代

首先,让我们来讨论下面两个问题:

如何枚举子数独?

可以使用 box_index = (row / 3) * 3 + columns / 3,其中 / 是整数除法。

如何确保行 / 列 / 子数独中没有重复项?

可以利用 value -> count 哈希映射来跟踪所有已经遇到的值。

现在,我们完成了这个算法的所有准备工作:

遍历数独。

检查看到每个单元格值是否已经在当前的行 / 列 / 子数独中出现过:

如果出现重复,返回 false

如果没有,则保留此值以进行进一步跟踪。

返回 true

  1. public boolean isValidSudoku(char[][] board) {
  2. Set seen = new HashSet();
  3. for (int i=0; i<9; ++i) {
  4. for (int j=0; j<9; ++j) {
  5. char number = board[i][j];
  6. if (number != '.')
  7. if (!seen.add(number + " in row " + i) ||
  8. !seen.add(number + " in column " + j) ||
  9. !seen.add(number + " in block " + i / 3 + "-" + j / 3))
  10. return false;
  11. }
  12. }
  13. return true;
  14. }

旋转图像


给定一个N×N的二维矩阵表示图像,90度顺时针旋转图像。

示例 :

  1. 输入: [[1,1,0,0],[1,0,0,1],[0,1,1,1],[1,0,1,0]]
  2. 输出: [[1,1,0,0],[0,1,1,0],[0,0,0,1],[1,0,1,0]]
  3. 解释: 首先翻转每一行: [[0,0,1,1],[1,0,0,1],[1,1,1,0],[0,1,0,1]];
  4. 然后反转图片: [[1,1,0,0],[0,1,1,0],[0,0,0,1],[1,0,1,0]]

说明:

  1. 1 <= A.length = A[0].length <= 20
  2. 0 <= A[i][j] <= 1

解题思路

我们先来看看每个元素在旋转的过程中是如何移动的:

这提供给我们了一个思路,将给定的矩阵分成四个矩形并且将原问题划归为旋转这些矩形的问题。

现在的解法很直接 -- 可以在第一个矩形中移动元素并且在 长度为 4 个元素的临时列表中移动它们。

  1. public void rotate(int[][] matrix) {
  2. if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
  3. return;
  4. }
  5. int length = matrix.length;
  6. for (int i = 0; i < length / 2; i++) {
  7. for (int j = 0; j < (length + 1) / 2; j++){
  8. int tmp = matrix[i][j];
  9. matrix[i][j] = matrix[length - j - 1][i];
  10. matrix[length -j - 1][i] = matrix[length - i - 1][length - j - 1];
  11. matrix[length - i - 1][length - j - 1] = matrix[j][length - i - 1];
  12. matrix[j][length - i - 1] = tmp;
  13. }
  14. }
  15. }

二进制 / 位运算


  1. 优点:
  2. 特定情况下,计算方便,速度快,被支持面广
  3. 如果用算数方法,速度慢,逻辑复杂
  4. 位运算不限于一种语言,它是计算机的基本运算方法

知识点预热


(一)按位与&

两位全为1,结果才为1

0&0=0;0&1=0;1&0=0;1&1=1

例如:51&5 即 0011 0011 & 0000 0101 =0000 0001 因此51&5=1.

特殊用法

(1)清零。如果想将一个单元清零,即使其全部二进制位为0,只要与一个各位都是零的数值相与,结果为零。

(2)取一个数中指定位

例如:设 X=10101110,取X的低四位,用X&0000 1111=0000 1110即可得到。

方法:找一个数,对应x要取的位,该数的对应位为1,其余位为零,此数与x进行“与运算”可以得到x中的指定位。

(二)按位或 |

只要有一个为1,结果就为1。

0|0=0; 0|1=1;1|0=1;1|1=1;

例如:51|5 即0011 0011 | 0000 0101 =0011 0111 因此51|5=55

特殊用法

常用来对一个数据的某些位置1。

方法:找到一个数,对应x要置1的位,该数的对应位为1,其余位为零。此数与x相或可使x中的某些位置1。

(三)异或 ^

两个相应位为“异”(值不同),则该位结果为1,否则为0

0^0=0; 0^1=1; 1^0=1; 1^1=0;

例如:51^5 即0011 0011 ^ 0000 0101 =0011 0110 因此51^5=54

特殊用法

(1) 与1相异或,使特定位翻转

方法:找一个数,对应X要翻转的位,该数的对应为1,其余位为零,此数与X对应位异或即可。

例如:X=1010 1110,使X低四位翻转,用X^0000 1111=1010 0001即可得到。

(2) 与0相异或,保留原值

例如:X^0000 0000 =1010 1110

(3)两个变量交换值

1.借助第三个变量来实现

C=A;A=B;B=C;

2.利用加减法实现两个变量的交换

A=A+B;B=A-B;A=A-B;

3.用位异或运算来实现,也是效率最高的

原理:一个数异或本身等于0 ;异或运算符合交换律

A=A^B;B=A^B;A=A^B

(四)取反与运算~

对一个二进制数按位取反,即将0变为1,1变0

  1. ~1=0 ;~0=1

(五)左移<<

将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0)

例如: 2<<1 =4 10<<1=100

若左移时舍弃的高位不包含1,则每左移一位,相当于该数乘以2

例如:

  1. 11(1011)<<2= 0010 1100=22
  2. 11(00000000 00000000 00000000 1011)整形32bit

(六)右移>>

将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃。若右移时舍高位不是1(即不是负数),操作数每右移一位,相当于该数除以2

左补0还是补1得看被移数是正还是负。

例如:4>>2=4/2/2=1

  1. -14(即1111 0010)>>2 =1111 1100=-4

(七)无符号右移运算>>>

各个位向右移指定的位数,右移后左边空出的位用零来填充,移除右边的位被丢弃

例如:-14>>>2

(即11111111 11111111 11111111 11110010)>>>2

=(00111111 11111111 11111111 11111100)=1073741820

只出现一次的数字


给出 2 * n + 1个数字,除其中一个数字之外其他每个数字均出现两次,找到这个数字。

异或运算具有很好的性质,相同数字异或运算后为0,并且具有交换律和结合律,故将所有数字异或运算后即可得到只出现一次的数字。

示例 :

  1. 输入: [4,1,2,1,2]
  2. 输出: 4

解题思路

如果我们对 0 和二进制位做 XOR 运算,得到的仍然是这个二进制位

\(a \oplus 0 = a\) \(a⊕0=a\)

如果我们对相同的二进制位做 XOR 运算,返回的结果是 0

\(a \oplus a = 0\) \(a⊕a=0\)

XOR 满足交换律和结合律

\(a \oplus b \oplus a = (a \oplus a) \oplus b = 0 \oplus b = ba⊕b⊕a=(a⊕a)⊕b=0⊕b=b\)

所以我们只需要将所有的数进行 XOR 操作,得到那个唯一的数字。

  1. public int singleNumber(int[] A) {
  2. if(A == null || A.length == 0) {
  3. return -1;
  4. }
  5. int rst = 0;
  6. for (int i = 0; i < A.length; i++) {
  7. rst ^= A[i];
  8. }
  9. return rst;
  10. }

复杂度分析

时间复杂度: O(n) 。我们只需要将 \(\text{nums}\) 中的元素遍历一遍,所以时间复杂度就是 \(\text{nums}\) 中的元素个数。

空间复杂度:O(1)

格雷编码

格雷编码是一个二进制数字系统,在该系统中,两个连续的数值仅有一个二进制的差异。给定一个非负整数 n ,表示该代码中所有二进制的总数,请找出其格雷编码顺序。一个格雷编码顺序必须以 0 开始,并覆盖所有的 2n 个整数。例子——输入:2;输出:[0, 1, 3, 2];解释: 0 - 001 - 013 - 112 - 10

解题思路

格雷码生成公式:G(i) = i ^ (i >> 2)

  1. public ArrayList<Integer> grayCode(int n) {
  2. ArrayList<Integer> result = new ArrayList<Integer>();
  3. for (int i = 0; i < (1 << n); i++) {
  4. result.add(i ^ (i >> 1));
  5. }
  6. return result;
  7. }

其他


整数反转


将一个整数中的数字进行颠倒,当颠倒后的整数溢出时,返回 0 (标记为 32 位整数)。

示例 :

  1. 输入: -123
  2. 输出: -321

解题思路

利用除 10 取余的方法,将最低位和最高倒序输出即可

  1. public int reverseInteger(int n) {
  2. int reversed_n = 0;
  3. while (n != 0) {
  4. int temp = reversed_n * 10 + n % 10;
  5. n = n / 10;
  6. if (temp / 10 != reversed_n) {
  7. reversed_n = 0;
  8. break;
  9. }
  10. reversed_n = temp;
  11. }
  12. return reversed_n;
  13. }

LRU缓存策略


运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put 。

获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。

写入数据 put(key, value) - 如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间。

示例:

  1. LRUCache cache = new LRUCache( 2 /* 缓存容量 */ );
  2. cache.put(1, 1);
  3. cache.put(2, 2);
  4. cache.get(1); // 返回 1
  5. cache.put(3, 3); // 该操作会使得密钥 2 作废
  6. cache.get(2); // 返回 -1 (未找到)
  7. cache.put(4, 4); // 该操作会使得密钥 1 作废
  8. cache.get(1); // 返回 -1 (未找到)
  9. cache.get(3); // 返回 3
  10. cache.get(4); // 返回 4

解题思路

解法一:

自定义数据结构:

  • 实现一个链表用于记录缓存,并处理调用使用频率
  • 定义一个 HashMap 用于记录缓存内容
  1. public class LRUCache {
  2. private class Node{
  3. Node prev;
  4. Node next;
  5. int key;
  6. int value;
  7. public Node(int key, int value) {
  8. this.key = key;
  9. this.value = value;
  10. this.prev = null;
  11. this.next = null;
  12. }
  13. }
  14. private int capacity;
  15. private HashMap<Integer, Node> hs = new HashMap<Integer, Node>();
  16. private Node head = new Node(-1, -1);// 头
  17. private Node tail = new Node(-1, -1);// 尾
  18. public LRUCache(int capacity) {
  19. this.capacity = capacity;
  20. tail.prev = head;
  21. head.next = tail;
  22. }
  23. public int get(int key) {
  24. if( !hs.containsKey(key)) { //key找不到
  25. return -1;
  26. }
  27. // remove current
  28. Node current = hs.get(key);
  29. current.prev.next = current.next;
  30. current.next.prev = current.prev;
  31. // move current to tail
  32. move_to_tail(current); //每次get,使用次数+1,最近使用,放于尾部
  33. return hs.get(key).value;
  34. }
  35. public void set(int key, int value) { //数据放入缓存
  36. // get 这个方法会把key挪到最末端,因此,不需要再调用 move_to_tail
  37. if (get(key) != -1) {
  38. hs.get(key).value = value;
  39. return;
  40. }
  41. if (hs.size() == capacity) { //超出缓存上限
  42. hs.remove(head.next.key); //删除头部数据
  43. head.next = head.next.next;
  44. head.next.prev = head;
  45. }
  46. Node insert = new Node(key, value); //新建节点
  47. hs.put(key, insert);
  48. move_to_tail(insert); //放于尾部
  49. }
  50. private void move_to_tail(Node current) { //移动数据至尾部
  51. current.prev = tail.prev;
  52. tail.prev = current;
  53. current.prev.next = current;
  54. current.next = tail;
  55. }
  56. }

解法二:

题目要求实现 LRU 缓存机制,需要在 O(1)时间内完成如下操作:

  • 获取键 / 检查键是否存在
  • 设置键
  • 删除最先插入的键
  • 前两个操作可以用标准的哈希表在 O(1) 时间内完成。

有一种叫做有序字典的数据结构,综合了哈希表链表,在 Java 中为 LinkedHashMap

下面用这个数据结构来实现。

  1. class LRUCache extends LinkedHashMap<Integer, Integer>{
  2. private int capacity;
  3. public LRUCache(int capacity) {
  4. super(capacity, 0.75F, true);
  5. this.capacity = capacity;
  6. }
  7. public int get(int key) {
  8. return super.getOrDefault(key, -1);
  9. }
  10. public void put(int key, int value) {
  11. super.put(key, value);
  12. }
  13. @Override
  14. protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
  15. return size() > capacity;
  16. }
  17. }

复杂度分析

  • 时间复杂度:对于 put 和 get 操作复杂度是 \(O(1)\),因为有序字典中的所有操作:
  • get/in/set/move_to_end/popitem(get/containsKey/put/remove)都可以在常数时间内完成。

    空间复杂度:\(O(capacity)\),因为空间只用于有序字典存储最多 capacity + 1 个元素。

Attention

  • 为了提高文章质量,防止冗长乏味

下一部分算法题

  • 本片文章篇幅总结越长。我一直觉得,一片过长的文章,就像一堂超长的 会议/课堂,体验很不好,所以我打算再开一篇文章

  • 在后续文章中,我将继续针对链表 队列 动态规划 矩阵 位运算 等近百种,面试高频算法题,及其图文解析 + 教学视频 + 范例代码,进行深入剖析有兴趣可以继续关注 _yuanhao 的编程世界

  • 不求快,只求优质,每篇文章将以 2 ~ 3 天的周期进行更新,力求保持高质量输出

相关文章


图文解析 2019 面试算法题「字符串处理 + 动态规划 汇总」

「面试原题 + 图文详解 + 实例代码」二叉搜索树-双指针-贪心 面试题汇总

面试高频算法题汇总「图文解析 + 教学视频 + 范例代码」之 二分 + 哈希表 + 堆 + 优先队列 合集

面试必备:高频算法题终章「图文解析 + 范例代码」之 矩阵 二进制 + 位运算 + LRU 合集的更多相关文章

  1. 面试必备:排序算法汇总(c++实现)

    排序算法主要考点: 7种排序 冒泡排序.选择排序.插入排序.shell排序.堆排序.快速排序.归并排序 以上排序算法是面试官经常会问到的算法,至于其他排序比如基数排序等等,这里不列举. 以下算法通过c ...

  2. 数据结构笔记02:Java面试必问算法题

    1. 面试的时候,栈和队列经常会成对出现来考察.本文包含栈和队列的如下考试内容: (1)栈的创建 (2)队列的创建 (3)两个栈实现一个队列 (4)两个队列实现一个栈 (5)设计含最小函数min()的 ...

  3. 《吊打面试官》系列-Redis终章_凛冬将至、FPX_新王登基

    你知道的越多,你不知道的越多 点赞再看,养成习惯 前言 Redis在互联网技术存储方面使用如此广泛,几乎所有的后端技术面试官都要在Redis的使用和原理方面对小伙伴们进行360°的刁难.作为一个在互联 ...

  4. 面试常见二叉树算法题集锦-Java实现

    1.求二叉树的深度或者说最大深度 /* ***1.求二叉树的深度或者说最大深度 */ public static int maxDepth(TreeNode root){ if(root==null) ...

  5. 笔试算法题(20):寻找丑数 & 打印1到N位的所有的数

    出题:将只包含2,3,5的因子的数称为丑数(Ugly Number),要求找到前面1500个丑数: 分析: 解法1:依次判断从1开始的每一个整数,2,3,5是因子则整数必须可以被他们其中的一个整除,如 ...

  6. GitHub 系列之「向GitHub 提交代码」

    1.SSH 你拥有了一个 GitHub 账号之后,就可以自由的 clone 或者下载其他项目,也可以创建自己的项目,但是你没法提交代码.仔细想想也知道,肯定不可能随意就能提交代码的,如果随意可以提交代 ...

  7. 从0开始学习 GITHUB 系列之「向GITHUB 提交代码」【转】

    本文转载自:http://stormzhang.com/github/2016/06/04/learn-github-from-zero4/ 版权声明:本文为 stormzhang 原创文章,可以随意 ...

  8. 趣题: 按二进制中1的个数枚举1~2^n (位运算技巧)

    ; ; k <= n; k++){ << k)-,u = << n; s < u;){ ;i < n;i++) printf(-i)&); print ...

  9. 「面试高频」二叉搜索树&双指针&贪心 算法题指北

    本文将覆盖 「字符串处理」 + 「动态规划」 方面的面试算法题,文中我将给出: 面试中的题目 解题的思路 特定问题的技巧和注意事项 考察的知识点及其概念 详细的代码和解析 开始之前,我们先看下会有哪些 ...

随机推荐

  1. Factory Method工厂方法模式

    定义一个用于创建对象的接口,让子类决定将哪一个类实例化.Factory Method使一个类的实例化延迟到其子类,属于创建型模式 在此模式中,工厂父类负责定义创建产品对象的公共接口,而工厂子类负责生产 ...

  2. android中shape 的使用

    android 开发中 对于 shape 和 selector的使用,一直都不是很熟练, 记录一下.便于以后参考. 举个项目中例子图 对于上面的2个radiobutton ,背景我们可以让美工做一个. ...

  3. VG有空间,创建逻辑卷

    1.查看VG空间 [root@CNSZ22PL2787 ~]# vgs VG #PV #LV #SN Attr VSize VFree VolGroup00 1 7 0 wz--n- 1.63t 1. ...

  4. 3、循环链表(java实现)

    1.节点类 public class Node<T> { public T data; public Node next; } 2.实现类 public class CircularLin ...

  5. spring boot 配置文件加密数据库用户名/密码

    这篇文章为大家分享spring boot的配置文件properties文件里面使用经过加密的数据库用户名+密码,因为在自己做过的项目中,有这样的需求,尤其是一些大公司,或者说上市公司,是不会把这些敏感 ...

  6. Spring Cloud异步场景分布式事务怎样做?试试RocketMQ

    一.背景 在微服务架构中,我们常常使用异步化的手段来提升系统的 吞吐量 和 解耦 上下游,而构建异步架构最常用的手段就是使用 消息队列(MQ),那异步架构怎样才能实现数据一致性呢?本文主要介绍如何使用 ...

  7. 2019 DevOps 必备面试题——配置管理篇

    原文地址:https://medium.com/edureka/devops-interview-questions-e91a4e6ecbf3 原文作者:Saurabh Kulshrestha 翻译君 ...

  8. Layer弹层(父子传值,兄弟传值)

    需求:最外面列表界面点修改弹出LayerA界面,再点击LayerA界面中的选择地图坐标按钮弹出LayerB地图界面 这个过程涉及到的: 1:LayerA将坐标传给LayerB,LayerB在地图上显示 ...

  9. (5)Makefile详解

         Makefile是一个自动化的编译工具,关系到整个工程的编译规则,极大的提高了软件开发的效率.     (1)Makefile的编译规则 //Makefile 也可以写作 makefile1 ...

  10. 这个注册的 IP 网络都不通了,Eureka 注册中心竟然无法踢掉它!

    本文导读: 微服务技术架构选型介绍 k8s 容器化部署架构方案 Eureka 注册中心问题场景 问题解决手段及原理剖析 阅读本文建议先了解: 注册中心基本原理 K8s(Kuberneters)基本概念 ...