笔者注:之前为了准备蓝桥杯等系列算法比赛写了很多算法博客,也真的让自己在算法方面提升很大,收获了很多奖项。现在,目标变成了【我想找一份实习】,所以,这一系列文章,将会以实习为导向,完成算法、八股文等多方面维度的学习和准备,希望能在最后拿到一份自己想要的实习。

本文为“算法篇”,学习路径源自大佬labuladong:https://labuladong.github.io/algo/

算法篇·

  • 数据结构
    • 链表
      • 21 合并两个有序链表(简单)
      • 86 分隔链表(中等)
      • ※23 合并K个升序链表(困难)
      • ※19 删除链表的倒数第 N 个结点(中等)
      • ※876 链表的中间结点(简单)
      • 环形链表三连问: 1. 是否有环(141) 2. 找出环的入口(142) 3. 环中节点个数
      • ※141 环形链表(简单)
      • ※142 环形链表Ⅱ(中等)
      • 【面试扩展】环中结点个数
      • 160 相交链表(简单)
      • 206 反转链表(简单)
      • 反转链表前N个结点
      • 92 反转链表 II(中等)
      • 25 K 个一组翻转链表(困难)
      • 234 回文链表(简单)
    • 数组
      • 26 删除有序数组中的重复项(简单)
      • 27 移除元素(简单)
      • 283 移动零(简单)
      • 167 两数之和 II - 输入有序数组(中等)
      • 344 反转字符串(简单)
      • 回文判断(简单)
      • 最长回文子串(中等)
      • 一维前缀和
      • 二维前缀和
      • 一维差分数组
      • 370 区间加法(中等)
      • 1109 航班预订统计(中等)
      • 1094 拼车(中等)
    • 二维数组的遍历
      • ※151 反转字符串中的单词(中等)
      • 48 旋转图像(中等)
      • 【面试扩展】矩阵逆时针旋转90°
      • ※54 螺旋矩阵(中等)
      • ※59 螺旋矩阵 II(中等)
    • 滑动窗口
      • 【经典面试题】※76 最小覆盖子串(困难)
      • 567 字符串的排列(中等)
      • 438 找到字符串中所有字母异位词(中等)
      • 3 无重复字符的最长子串(中等)
      • 187 重复的DNA序列(中等)
    • 二分
      • 【经典面试题】※34 在排序数组中查找元素的第一个和最后一个位置(中等)
      • 【经典面试题】33 搜索旋转排序数组(中等)
      • 81 搜索旋转排序数组 II(中等)
      • 4 寻找两个正序数组的中位数(困难)
      • 380 O(1) 时间插入、删除和获取随机元素(中等)

数据结构

链表

21 合并两个有序链表(简单)

基本操作,注意使用虚拟头结点,方便结果处理

class Solution {public ListNode mergeTwoLists(ListNode list1, ListNode list2) {// 虚拟头结点ListNode dummy = new ListNode();ListNode p = dummy;while (list1 != null && list2 != null) {if (list1.val > list2.val) {p.next = list2;list2 = list2.next;} else {p.next = list1;list1 = list1.next;}// 新链表的节点继续往下走p = p.next;}if (list1 == null) {// 说明list2可能还有剩p.next = list2;}if (list2 == null) {// 说明list1可能还有剩p.next = list1;}return dummy.next;}
}

86 分隔链表(中等)

具体来说,我们可以把原链表分成两个小链表,一个链表中的元素大小都小于 x,另一个链表中的元素都大于等于 x,最后再把这两条链表接到一起,就得到了题目想要的结果。

注意合并时,要将p2.next置为null,否则当最后一个节点值小于x时,合并后会成环,可以画图试试

class Solution {public ListNode partition(ListNode head, int x) {ListNode dummy1 = new ListNode();ListNode dummy2 = new ListNode();ListNode p1 = dummy1;ListNode p2 = dummy2;while (head != null) {if (head.val < x) {// 放p1中p1.next = head;p1 = p1.next;} else {// 放p2中p2.next = head;p2 = p2.next;}head = head.next;}// 连接两个链表,注意要让更大的链表的next=null否则当最后一个节点值小于x时,会成环p2.next = null;p1.next = dummy2.next;return dummy1.next;}
}

※23 合并K个升序链表(困难)

合并 k 个有序链表的逻辑类似合并两个有序链表,难点在于,如何快速得到 k 个节点中的最小节点,接到结果链表上?

这里我们就要用到 优先级队列(二叉堆) 这种数据结构,把链表节点放入一个最小堆,就可以每次获得 k 个节点中的最小节点:

class Solution {public ListNode mergeKLists(ListNode[] lists) {PriorityQueue<ListNode> pq = new PriorityQueue<>(new Comparator<>(){@Overridepublic int compare(ListNode a, ListNode b) {return a.val - b.val;  // 降序排列}});for (ListNode node : lists) {if (node != null) {// 先只添加k个链表的头结点,后面在遍历时把头结点之后的结点加入pq.add(node);}}// 和前面合并有序链表一样ListNode dummy = new ListNode();ListNode p = dummy;while (!pq.isEmpty()) {// 获取所有链表的当前最小节点放入新链表中ListNode tmp = pq.poll();p.next = tmp;// 对于当前拿走了一个结点的链表,还需要把剩余节点加入pq队列if (tmp.next != null) {pq.add(tmp.next);} // 新链表的节点继续往前走p = p.next;}return dummy.next;}
}

※19 删除链表的倒数第 N 个结点(中等)

这道题涉及一个问题:如何快速找到倒数第N个节点?

画图解决:
1、先让一个节点p1,从头结点开始先走N步
2、引入一个新的节点p2,指向头结点
3、p1、p2同时走,当p1 == null时,p2所在位置即为倒数第N个节点

ListNode p1 = head;
// 先走k步
for (int i = 0; i < k; i++) {p1 = p1.next;
}
ListNode p2 = head;
// 再同时走
while (p1 != null) {p2 = p2.next;p1 = p1.next;
}
// 最后p2所指即为所需节点
return p2;

但是本题要删除倒数第N个节点,那我们就得找到倒数第N+1个节点,但是为了避免需要删除头结点而找不到第N+1个节点的情况,可以给原始链表加上一个虚拟头结点:

class Solution {public ListNode removeNthFromEnd(ListNode head, int n) {// 虚拟头结点,给head再增加一个节点,避免删除头结点时出错// 如果链表长度=4,n=4,我们现在要去找第n+1个节点,找不到呀?ListNode dummy = new ListNode();dummy.next = head;// 有了虚拟头结点就可以处理删除头结点的问题ListNode p1 = dummy;// 删除倒数第 n 个,要先找倒数第 n + 1 个节点n += 1;for (int i = 0; i < n; i++) {p1 = p1.next;}// 再来个p2 和 p1一起走ListNode p2 = dummy;while (p1 != null) {p1 = p1.next;p2 = p2.next;}// 现在p2所处位置就是要删除节点的前一个节点// 删掉倒数第 n 个节点p2.next = p2.next.next;return dummy.next;}
}

※876 链表的中间结点(简单)

经典问题了,之前也刷过几遍,就是快慢指针

class Solution {public ListNode middleNode(ListNode head) {ListNode slow = head;ListNode fast = head;while (fast != null && fast.next != null) {slow = slow.next;fast = fast.next.next;  // 如果是偶数个节点,返回的是中间靠后的节点}return slow;}
}

环形链表三连问: 1. 是否有环(141) 2. 找出环的入口(142) 3. 环中节点个数

※141 环形链表(简单)

同上,依旧使用快慢指针,如果有环,则快指针必然会和慢指针相遇:

public class Solution {public boolean hasCycle(ListNode head) {ListNode slow = head;ListNode fast = head;while (fast != null && fast.next != null) {slow = slow.next;fast = fast.next.next;if (slow == fast) return true;}return false;}
}

※142 环形链表Ⅱ(中等)

在上一题基础上需要找到入环结点,也是模板题:

关键是让slow指针最后要重新指向头结点,然后再让slow、fast指针一起走,最后相遇的地方就是入环结点

public class Solution {public ListNode detectCycle(ListNode head) {ListNode slow = head;ListNode fast = head;// 先判断有无环while (fast != null && fast.next != null) {slow = slow.next;fast = fast.next.next;if (slow == fast) {// 成环了break;}}// 若无环则直接nullif (fast == null || fast.next == null) {return null;}// 让slow指针指向头结点,再和fast指针一起走,直到两者相遇点就是入环点slow = head;while (slow != fast) {slow = slow.next;fast = fast.next;}return slow;}
}

【面试扩展】环中结点个数

slow与fast相遇在环的入口, 让fast或者slow单独在环里再走一圈, 并进行计数, 当fast.next = slow时, 返回count+1,就是环中节点的个数。

160 相交链表(简单)

如果两个链表的长度一致的话还好处理,直接从head开始往后遍历,判断有相同的结点[==],就说明是相交的,关键是长度不相同,但注意:相交点之后的两条链表合为一条链,那我们就让长的链表先把多的结点数走掉,再让两个链表一起走,如果相交,走到后面总会有结点一致;如果不相交,到最后都为null,也一样了。

public class Solution {public ListNode getIntersectionNode(ListNode headA, ListNode headB) {ListNode pA = headA;ListNode pB = headB;int lenA = 0;int lenB = 0;while (pA != null) {lenA++;pA = pA.next;}while (pB != null) {lenB++;pB = pB.next;}// 让长的链表先走一段if (lenA > lenB) {for (int i = 0; i < lenA - lenB; i++) {headA = headA.next;}        } else {for (int i = 0; i < lenB - lenA; i++) {headB = headB.next;}}// 最后再判断是否相交,不相交也能正确返回nullwhile (headA != headB) {headA = headA.next;headB = headB.next;}return headA;}
}

206 反转链表(简单)

用递归可以写得很简洁明了,因为递归的重点在于函数含义的理解,而不用真正搞懂内部递归调用的过程。

递归函数定义为:输入一个节点 head,将「以 head 为起点」的链表反转,并返回反转之后的头结点。

那么,就可以画图进行分析:




关键是最后这里,head.next = null,一方面是断开head正向的连接,另一方面也让反转后的最后一个节点的next=null。

同时要注意base基础条件,没有结点、或只有一个结点,反转就是自身,直接返回即可。

// 定义:输入一个单链表头结点,将该链表反转,返回新的头结点
ListNode reverse(ListNode head) {if (head == null || head.next == null) {return head;}ListNode last = reverse(head.next);head.next.next = head;head.next = null;return last;
}

迭代解法:

/*** 迭代方法* 1 -> 2 -> 3 -> 4 -> null* null <- 1 <- 2 <- 3 <- 4** @param head* @return*/public static ListNode reverseListIterative(ListNode head) {ListNode prev = null; //前指针节点ListNode curr = head; //当前指针节点//每次循环,都将当前节点指向它前面的节点,然后当前节点和前节点后移while (curr != null) {ListNode nextTemp = curr.next; //临时节点,暂存当前节点的下一节点,用于后移curr.next = prev; //将当前节点指向它前面的节点prev = curr; //前指针后移curr = nextTemp; //当前指针后移}return prev;}

反转链表前N个结点

// 将链表的前 n 个节点反转(n <= 链表长度)
ListNode reverseN(ListNode head, int n)


之前反转是让头结点的next直接等于null,但是这里需要将头结点的next连接到后驱结点(第n+1个结点),因为只需要反转一部分。

class Solution {ListNode postNode;public ListNode reverseList(ListNode head, int n) {if (n == 1) {  // 到达了需要反转的最后一个结点// n==1是因为没有设置dummy虚拟头结点,n减为1时,就到了第n个结点postNode = head.next;  // 记录n+1后驱结点return head;  // 避免只有单个结点的情况}// 下面和反转整个链表的过程差不多,区别就是最后还需要连接到后驱节点上// 以head.next为起点,反转n-1个结点ListNode newHead = reverseList(head.next, n - 1);  head.next.next = head;// 这里下一个结点要连接到后驱节点上head.next = postNode;return newHead;}
}

92 反转链表 II(中等)

现在需要按照题目需求,反转一个范围内[m, n]的链表,链表的索引从1开始,当m==1时,就相当于上一题反转前N个结点,当m != 1时,怎么办?

如果我们把 head 的索引视为 1,那么我们是想从第 m 个元素开始反转对吧;如果把 head.next 的索引视为 1 呢?那么相对于 head.next,反转的区间应该是从第 m - 1 个元素开始的;那么对于 head.next.next 呢……

class Solution {public ListNode reverseBetween(ListNode head, int left, int right) {if (left == 1) {// 就相当于是反转前right个结点return reverseN(head, right);}// 如果不是从第一个结点开始,那就把下一个结点当作第一个结点head.next = reverseBetween(head.next, left - 1, right - 1);return head;}// 反转前N个结点的函数,别忘了记录后驱结点ListNode postNode = null;public ListNode reverseN(ListNode head, int n) {if (n == 1) {postNode = head.next;  // 找到n+1结点作为后去节点return head;}ListNode newHead = reverseN(head.next, n - 1);head.next.next = head;// 这里要连接到后驱结点head.next = postNode;return newHead;}
}

迭代写法:穿针引线法,本质就是独立出需要反转的那部分链表结点

class Solution {public ListNode reverseBetween(ListNode head, int left, int right) {ListNode dummy = new ListNode();dummy.next = head;// 记录四个位置,两个用来断开连接,两个用来连接ListNode preLeft, postRight, leftNode, rightNode;preLeft = dummy;// 先找到leftNode的前一个结点for (int i = 0; i < left - 1; i++) {preLeft = preLeft.next;}leftNode = preLeft.next;// 记录找rightNoderightNode = leftNode;for (int i = 0; i < right - left; i++) {rightNode = rightNode.next;}postRight = rightNode.next;// 找完之后先断开连接preLeft.next = null;rightNode.next = null;// 反转以leftNode开始的结点ListNode newHead = reverse(leftNode);// 将三坨连接起来preLeft.next = newHead;leftNode.next = postRight;return dummy.next;}// 简单的链表反转函数public ListNode reverse(ListNode head) {ListNode pre = null;ListNode cur = head;while (cur != null) {ListNode tmp = cur.next;cur.next = pre;pre = cur;  // pre指针往后移动cur = tmp;}return pre;}
}

如果想要反转一个区间[a,b)范围内的结点,该怎么做呢?

只需要将cur != null,改成cur != b即可

    public ListNode reverse(ListNode head, ListNode b) {ListNode pre = null;ListNode cur = head;while (cur != b) {ListNode tmp = cur.next;cur.next = pre;pre = cur;  // pre指针往后移动cur = tmp;}return pre;}

值得一提的是,递归操作链表并不高效。和迭代解法相比,虽然时间复杂度都是 O(N),但是迭代解法的空间复杂度是 O(1),而递归解法需要堆栈,空间复杂度是 O(N)

25 K 个一组翻转链表(困难)

从头结点开始,按照k个结点一组进行反转,若剩下结点不足k个则保持原序

只要限定出需要反转结点的区间就可以,例如:[a, b),那么,我们先反转这个区间内的结点,返回这个区间新的头结点,现在,a结点就是当前区间的尾结点,需要将其连接到下一块区间,而下一块区间的头结点又需要递归调用同样的函数来获取,至此用递归就完成了本题的求解:

class Solution {public ListNode reverseKGroup(ListNode head, int k) {ListNode a = head;ListNode b = head;// 找到区间[a,b)for (int i = 0; i < k; i++) {if (b == null) return head;  // 不足k个直接返回头结点,不用反转b = b.next;}// 反转前k个结点ListNode newHead = reverse(a, b);// 反转后a作为尾结点,需要连接后续分组结点a.next = reverseKGroup(b, k);  // 下一个分组的起始结点就是breturn newHead;}public ListNode reverse(ListNode a, ListNode b) {ListNode pre = null;ListNode cur = a;// 和普通反转链表的区别就在于while的判断条件while (cur != b) {ListNode tmp = cur.next;cur.next = pre;pre = cur;cur = tmp;}return pre;}
}

234 回文链表(简单)

很简单,先用快慢指针找到中间结点,然后从中间节点开始反转后面的结点,反转之后再从头开始一个个元素比较即可:

就是两个模板的结合,注意快慢指针在偶数结点时,找到的是靠后的结点

class Solution {public boolean isPalindrome(ListNode head) {ListNode slow = head;ListNode fast = head;while (fast != null && fast.next != null) {slow = slow.next;fast = fast.next.next;}// slow即为中间节点,以它为起点反转后半部分链表ListNode last = reverse(slow);ListNode first = head;while (last != null) {if (last.val != first.val) return false;last = last.next;first = first.next;}return true;}public ListNode reverse(ListNode head) {ListNode pre = null;ListNode cur = head;while (cur != null) {ListNode tmp = cur.next;cur.next = pre;pre = cur;cur = tmp;}return pre;}
}

总结下,链表无非就那几个模板:整个反转、区间反转、快慢指针判断环、快慢指针找入环点、快慢指针找中间节点,更多是偏向模板向的题目。

数组

数组的算法主要考察:双指针、滑动窗口、二分,实际上本质都是双指针的扩展用法,用双指针时一定要注意:数组是否有序

26 删除有序数组中的重复项(简单)

难点在于用双指针实现原地修改,本题返回的是删除后的数组元素个数

先用fast指针去探路,slow刚开始就指向原始数组的开始位置,一旦fast != slow,就要把fast对应的元素存进来,因为是没有重复的。

本题给的数组是有序的

class Solution {public int removeDuplicates(int[] nums) {int slow = 0;int fast = 0;while (fast < nums.length) {if (nums[slow] != nums[fast]) {slow++;nums[slow] = nums[fast];}// 不管是否相等fast都要前进fast++;}// 因为slow下标从0开始,所以个数要+1return slow + 1;}
}

27 移除元素(简单)

本题给的数组是无序的,但本题已经告诉了需要移除的元素,反而更简单了,用一个指针保存数据,另外一个指针去查数据

class Solution {public int removeElement(int[] nums, int val) {int slow = 0;int fast = 0;while (fast < nums.length) {if (nums[fast] != val) {nums[slow++] = nums[fast]; }fast++;}return slow;}
}

283 移动零(简单)

无非就是把上一题的val特殊化为0,先找不是0的,然后再补0

class Solution {public void moveZeroes(int[] nums) {int len = nums.length;int slow = 0;int fast = 0;while (fast < len) {if (nums[fast] != 0) {nums[slow++] = nums[fast];}fast++;}// 剩下的位置补零while (slow < len) {nums[slow++] = 0;}}
}

167 两数之和 II - 输入有序数组(中等)

这是一系列属于nSum系列的题目,之后会专门对该专题进行练习

本题有点类似与二分、滑动窗口,本质是通过双指针 逼近/等于 目标答案:

class Solution {public int[] twoSum(int[] numbers, int target) {int left = 0;int right = numbers.length - 1;while (left < right) {int sum = numbers[left] + numbers[right];if (sum == target) {return new int[] {left + 1, right + 1};} else if (sum < target) {// 让sum更大一些,因为数组已经有序,所以才能用双指针left++;} else {// 让sum更小一些right--;}}return new int[] {-1, -1};}
}

344 反转字符串(简单)

简单的双指针运用

class Solution {public void reverseString(char[] s) {int len = s.length;int left = 0;int right = len - 1;while (left != len / 2) {char c = s[left];s[left] = s[right];s[right] = c;left++;right--;}}
}

回文判断(简单)

最长回文子串(中等)

这是一道经典的DP问题,但这里使用上一题中使用的中心扩展法进行求解,在空间复杂度上会更优一些

注意:子串和子序列的区别,子串是不允许中间删除某个元素,而子序列允许中间删除某个元素

找回文串的难点在于,回文串的的长度可能是奇数也可能是偶数,解决该问题的核心是从中心向两端扩散的双指针技巧

// 在 s 中寻找以 s[l] 和 s[r] 为中心的最长回文串,两个中心是因为当长度为偶数时就会有2个中心
String palindrome(String s, int l, int r) {// 防止索引越界while (l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r)) {// 双指针,向两边展开l--; r++;}// 返回以 s[l] 和 s[r] 为中心的最长回文串return s.substring(l + 1, r);
}

l = r时,就是奇数长度回文
r = l + 1时,就是偶数长度回文

注意substring函数是取[a, b-1],这个区间内的子串,所以left需要+1,而right不用管,因为会自己-1

class Solution {public String longestPalindrome(String s) {String ans = "";for (int i = 0; i < s.length(); i++) {// 以s[i]作为中心的最长回文子串,当然这个中心可能是偶数长度、奇数长度String s1 = getStr(s, i, i);  // 奇数长度String s2 = getStr(s, i, i + 1);  // 偶数长度[函数内部有范围检测不用担心越界]ans = s1.length() > ans.length() ? s1 : ans;ans = s2.length() > ans.length() ? s2 : ans;}return ans;}public String getStr(String s, int l, int r) {while (l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r)) {// 向两边扩展l--;r++;}return s.substring(l + 1, r);}
}

下面是经典的前缀和问题,纯属模板

一维前缀和

303 区域和检索 - 数组不可变

class NumArray {public int[] preSum;public NumArray(int[] nums) {int len = nums.length;preSum = new int[len + 1];for (int i = 0; i < len; i++) {preSum[i + 1] = preSum[i] + nums[i];}}public int sumRange(int left, int right) {return preSum[right + 1] - preSum[left];}
}

二维前缀和

304 二维区域和检索 - 矩阵不可变

二维比较难理解一些,可以画图,看新的区域是如何通过原始数组元素 和 之前计算好的二维preSum数组得到:

class NumMatrix {public int[][] preSum;public NumMatrix(int[][] matrix) {int m = matrix.length;int n = matrix[0].length;preSum = new int[m + 1][n + 1];// 根据画图得到算法for (int i = 0; i < m; i++) {for (int j = 0; j < n; j++) {preSum[i + 1][j + 1] = preSum[i][j + 1] + preSum[i + 1][j] + matrix[i][j] - preSum[i][j];}}}// 注意这里的下标从0开始,但是preSum中需要下标从1开始public int sumRegion(int row1, int col1, int row2, int col2) {return preSum[row2 + 1][col2 + 1] - preSum[row1][col2 + 1] - preSum[row2 + 1][col1] + preSum[row1][col1];}
}

讲了前缀和,肯定要来讲讲前缀和的逆问题,也就是:差分数组,当然也有一维、二维差分数组,这里只介绍一维(找工作来说完全够了,不是为了打算法比赛哈哈哈哈)

一维差分数组

差分数组的主要适用场景是频繁对原始数组的某个区间的元素进行增减

int[] diff = new int[nums.length];
// 构造差分数组
diff[0] = nums[0];
for (int i = 1; i < nums.length; i++) {diff[i] = nums[i] - nums[i - 1];
}
int[] res = new int[diff.length];
// 根据差分数组构造结果数组
res[0] = diff[0];
for (int i = 1; i < diff.length; i++) {res[i] = res[i - 1] + diff[i];
}

如果要对区间进行增、减元素该怎么做?

举个例子,对于数组:{1,2,2,3,1,2}
差分数组为:{1,1,0,1,-2,1}

现在我们要给下标从1-3的元素都+1,原数组变为:{1,3,3,4,1,2}
差分数组为:{1,2,0,1,-3,1}

从哪一位开始加,哪一位对应的差分数组加上val,到结束的位的下一位,减去val,这样才能保证后面的值不会受到前面+val的影响:

// 假设给区间[i,j] + val
diff[i] += val;
if (j + 1 < diff.length)// 如果j + 1 >= diff.length 说明整个数组都需要+val,也就不用在后面-valdiff[j + 1] -= val;

370 区间加法(中等)

    int[] getModifiedArray(int length, int[][] updates) {int[] diff = new int[length];for (int[] first : updates) {int i = first[0];int j = first[1];int val = first[2];// 标准的差分+-val步骤diff[i] += val;if (j + 1 < length) {diff[j + 1] -= val;}}// 原汤化原食,直接用旧数组来做for (int i = 1; i < length; i++) {diff[i] += diff[i - 1];}return diff;}

1109 航班预订统计(中等)

就是上一题原封不动:

class Solution {public int[] corpFlightBookings(int[][] bookings, int n) {// 差分数组int[] diff = new int[n];for (int[] first : bookings) {int i = first[0] - 1;int j = first[1] - 1;int val = first[2];diff[i] += val;if (j + 1 < n) {diff[j + 1] -= val;}}for (int i = 1; i < n; i++) {diff[i] += diff[i - 1];}return diff;}
}

1094 拼车(中等)

class Solution {public boolean carPooling(int[][] trips, int capacity) {int[] diff = new int[1000 + 5];for (int[] first : trips) {int num = first[0];int i = first[1];// first[2]时已经下车了,所以有num个乘客的范围应该是from - (to - 1)int j = first[2] - 1;  diff[i] += num;diff[j + 1] -= num;}// 因为下面计算result数组时要从i=1,这里要提前判断diff[0]if (diff[0] > capacity) return false;for (int i = 1; i <= 1000; i++) {diff[i] += diff[i - 1];if (diff[i] > capacity) return false;}return true;}
}

二维数组的遍历

这系列的问题中主要需要解决类似于螺旋矩阵这种,非常细节的模拟题目,主要是考察数组的遍历问题

先来看看这道原地反转字符串(经典面试题)

※151 反转字符串中的单词(中等)

解决思路分三步:因为题目中给出的字符串可能前后有空格,且单词间的空格至少有1个,也可能有多个,所以需要先把多余的空格去除,保证单词间的空格只有一个。之后,再整个字符串反转。然后,再对反转后字符串中的逐个单词进行反转即可。

class Solution {public String reverseWords(String s) {StringBuilder sb = trimSpace(s);// 反转整个字符串reverse(sb, 0, sb.length() - 1);// 逐个单词反转reverseEachWord(sb);return sb.toString();}// 逐个单词反转public void reverseEachWord(StringBuilder sb) {int len = sb.length();int left = 0;int right = 0;while (left < len) {// right = 单词最后一位后面的空格,当然最后一个单词依靠len来限制while (right < len && sb.charAt(right) != ' ') {right++;}// 翻转单词reverse(sb, left, right - 1);// 更新新的单词起始位置left = right + 1;right++;}}// 反转整个字符串,直接在sb对象的基础上,这里传的是引用的拷贝,和原引用同时指向同一对象public void reverse(StringBuilder sb, int left, int right) {while (left < right) {char c = sb.charAt(left);// 反转字符串sb.setCharAt(left, sb.charAt(right));sb.setCharAt(right, c);left++;right--;}}// 去除前后及单词间的多余空格,因为单词间的空格数"至少为1",需要将空格数减少为1public StringBuilder trimSpace(String s) {int left = 0;int right = s.length() - 1;while (left <= right && s.charAt(left) == ' ') left++;while (right >= 0 && s.charAt(right) == ' ') right--;// 现在左右指针都是字符了StringBuilder sb = new StringBuilder();while (left <= right) {char c = s.charAt(left);if (c != ' ') {// 不为空格,那就是字母sb.append(c);} else if (sb.charAt(sb.length() - 1) != ' ') {// 为空格,那么前面一个就不能为空格sb.append(c);}left++;}return sb;}
}

48 旋转图像(中等)

顺时针旋转90° = 矩阵按照对角线对称 + 逐行反转,最终结果即得,也就是说将旋转90°才能得到的结果进行分解转换

class Solution {public void rotate(int[][] matrix) {// 对角线对称int m = matrix.length;int n = matrix[0].length;for (int i = 0; i < m; i++) {for (int j = i; j < n; j++) {int tmp = matrix[i][j];matrix[i][j] = matrix[j][i];matrix[j][i] = tmp;}}// 逐行反转for (int i = 0; i < m; i++) {int left = 0;int right = n - 1;while (left < right) {int tmp = matrix[i][left];matrix[i][left] = matrix[i][right];matrix[i][right] = tmp;left++;right--;}}}
}

【面试扩展】矩阵逆时针旋转90°

只需要按照副对角线对称,再逐行反转即可

取一两个元素的对称关系出来,才能知道对应的通用对称关系

// 将二维矩阵原地逆时针旋转 90 度
void rotate2(int[][] matrix) {int n = matrix.length;// 沿左下到右上的对角线镜像对称二维矩阵for (int i = 0; i < n; i++) {for (int j = 0; j < n - i; j++) {// swap(matrix[i][j], matrix[n-j-1][n-i-1])int temp = matrix[i][j];matrix[i][j] = matrix[n - j - 1][n - i - 1];matrix[n - j - 1][n - i - 1] = temp;}}// 然后反转二维矩阵的每一行for (int[] row : matrix) {reverse(row);}
}

※54 螺旋矩阵(中等)

借用大佬的图:


就是按照题目进行遍历即可,但是要注意,横向遍历时,最外层限制因素应该是纵向,即upper_bound - lower_bound,内层限制因素就是left_bound - right_bound;而纵向遍历时,最外层限制因素应该是横向。

upper_bound 和 lower_bound组合,left_bound 和 right_bound组合

class Solution {public List<Integer> spiralOrder(int[][] matrix) {int m = matrix.length;int n = matrix[0].length;int upperBound = 0;int leftBound = 0;int rightBound = n - 1;int lowerBound = m - 1;List<Integer> ans = new LinkedList<>();// 需要横向遍历的,外层限制条件为纵向,内层限制条件为横向(注意遍历方向)while (ans.size() < m * n) {//  按照遍历顺序进行模拟// 顶部从左到右if (upperBound <= lowerBound) {for (int i = leftBound; i <= rightBound; i++) {ans.add(matrix[upperBound][i]);}// 上边界下移upperBound++;}// 右侧从上到下if (leftBound <= rightBound) {for (int i = upperBound; i <= lowerBound; i++) {ans.add(matrix[i][rightBound]);}// 右边界左移rightBound--;}// 底部从右向左if (upperBound <= lowerBound) {for (int i = rightBound; i >= leftBound; i--) {ans.add(matrix[lowerBound][i]);}// 下边界上移lowerBound--;}// 底部从下到上if (leftBound <= rightBound) {for (int i = lowerBound; i >= upperBound; i--) {ans.add(matrix[i][leftBound]);}// 左边界右移leftBound++;}}return ans;}
}

※59 螺旋矩阵 II(中等)

上一题的实现

class Solution {public int[][] generateMatrix(int n) {// 相比于之前的更好做了,因为是正方形int upperBound = 0;int leftBound = 0;int lowerBound = n - 1;int rightBound = n - 1;int[][] matrix = new int[n][n];int num = 1;  // 遍历数字while (num <= n * n) {// 按照顺序进行遍历if (upperBound <= lowerBound) {for (int i = leftBound; i <= rightBound; i++) {matrix[upperBound][i] = num++;}// 上边界下移upperBound++;}if (leftBound <= rightBound) {for (int i = upperBound; i <= lowerBound; i++) {matrix[i][rightBound] = num++;}// 右边界左移rightBound--;}if (upperBound <= lowerBound) {for (int i = rightBound; i >= leftBound; i--) {matrix[lowerBound][i] = num++;}// 下边界上移lowerBound--;}if (leftBound <= rightBound) {for (int i = lowerBound; i >= upperBound; i--) {matrix[i][leftBound] = num++;}// 左边界右移leftBound++;}}return matrix;}
}

滑动窗口

这个算法技巧的思路非常简单,就是维护一个窗口,不断滑动,然后更新答案

滑动窗口类题目其实模板很简单,难在细节处理上,如何把握好细节,调试bug是很难的:

/* 滑动窗口算法框架 */
void slidingWindow(string s) {unordered_map<char, int> window;int left = 0, right = 0;while (right < s.size()) {// c 是将移入窗口的字符char c = s[right];// 增大窗口right++;// 进行窗口内数据的一系列更新.../*** debug 输出的位置 ***/printf("window: [%d, %d)\n", left, right);/********************/// 判断左侧窗口是否要收缩while (window needs shrink) {// d 是将移出窗口的字符char d = s[left];// 缩小窗口left++;// 进行窗口内数据的一系列更新...}}
}

大致思路是:先维护一个窗口,窗口大小由left、right维护,刚开始left、right=0,接下来就需要扩大窗口,也就是让right++,光扩大了,怎么找答案呢?这就需要在合适的时间点进行收缩,也就是控制left++,这样就是一个窗口在滑动。

【经典面试题】※76 最小覆盖子串(困难)

直接看代码注释就可以:

需要注意,Integer类型,Integer常量池里放的数字范围是:-128-127,在这个范围内的数字会直接去常量池中拿,不会自动封装成Integer对象,但是!如果超过这个范围,就会自动封装,这个时候你再用==去判断大小是否相等就有问题了,因为这个时候判断的是引用地址是否相同,应该调用equals判断内容是否相等,和String类一样的道理。

class Solution {public String minWindow(String s, String t) {// 返回 s 中涵盖 t 所有字符的最小子串// 需要凑齐的字符数HashMap<Character, Integer> need = new HashMap<>();// 目前窗口中有的字符情况HashMap<Character, Integer> have = new HashMap<>();// 记录窗口中有效的字符数,也是缩小窗口的条件int valid = 0;// 记录答案int start = 0, len = Integer.MAX_VALUE;for (int i = 0; i < t.length(); i++) {need.put(t.charAt(i), need.getOrDefault(t.charAt(i), 0) + 1);}// 控制滑动窗口int left = 0, right = 0;while (right < s.length()) {char c = s.charAt(right);// 扩大窗口right++;// 当前字符是所需字符if (need.containsKey(c)) {have.put(c, have.getOrDefault(c, 0) + 1);if (need.get(c).equals(have.get(c))) {// 字符数相同,说明当前这“种”字符已经收集齐valid++;}}// 判断是否可以缩小窗口while (valid == need.size()) {// 满足题意,更新答案,找最小子串if (right - left < len) {len = right - left;start = left;}// 被移除的字符char d = s.charAt(left);// 缩小窗口left++;// 看这个字符是不是需要的if (need.containsKey(d)) {// 只有这种字符不够用时,valid才-1if (have.get(d).equals(need.get(d))) {valid--;}have.put(d, have.get(d) - 1);}}}return len == Integer.MAX_VALUE ? "" : s.substring(start, start + len);}
}

567 字符串的排列(中等)

关键是窗口缩小的判断条件,当窗口的大小>=目标串的大小时,就可以开始

class Solution {public boolean checkInclusion(String s1, String s2) {// 判断排列,也就是说只用判断s2中有没有一个子串的各种字符个数和s1匹配即可HashMap<Character, Integer> need = new HashMap<>();HashMap<Character, Integer> have = new HashMap<>();// 先看需要哪些字符for (int i = 0; i < s1.length(); i++) {char c = s1.charAt(i);need.put(c, need.getOrDefault(c, 0) + 1);}// 判断是否满足题意int valid = 0;// 控制窗口int left = 0, right = 0;while (right < s2.length()) {// 扩大窗口,获取right所指字符char c = s2.charAt(right);right++;if (need.containsKey(c)) {have.put(c, have.getOrDefault(c, 0) + 1);// 看当前这种字符是否满足题目要求if (have.get(c).equals(need.get(c))) {valid++;}}// 尝试缩小窗口,只要还没有满足题目含义就可以往后找// 只要找到有一个满足的就行// 这里由于right提前+1了,所以这里不用+1while (right - left >= s1.length()) {  // 当窗口大小>=s1,就已经能说明是否找到了// 没有找到就需要赶紧缩小窗口,让窗口还能继续扩展// 找到了if (valid == need.size()) {return true;}// 左移char d = s2.charAt(left);left++;if (need.containsKey(d)) {if (need.get(d).equals(have.get(d))) {valid--;}have.put(d, have.get(d) - 1);}}}return false;}
}

438 找到字符串中所有字母异位词(中等)

先看看错解:

class Solution {public List<Integer> findAnagrams(String s, String p) {List<Integer> ans = new LinkedList<>();  // 记录答案HashMap<Character, Integer> need = new HashMap<>();HashMap<Character, Integer> have = new HashMap<>();// 统计需要字符数for (int i = 0; i < p.length(); i++) {char c = p.charAt(i);need.put(c, need.getOrDefault(c, 0) + 1);}int left = 0, right = 0;int valid = 0;while (right < s.length()) {// 扩大窗口char c = s.charAt(right);right++;if (need.containsKey(c)) {have.put(c, have.getOrDefault(c, 0) + 1);if (need.get(c).equals(have.get(c))) {valid++;}}// 满足题目要求开始缩小窗口,并更新答案while (valid == need.size()) {char d = s.charAt(left);left++;if (need.containsKey(d)) {if (need.get(d) == have.get(d)) {valid--;// 在这里找答案,因为一定能保证left为开始索引ans.add(left - 1);}have.put(d, have.get(d) - 1);}}}return ans;}
}

错在哪里?窗口的缩小条件!相信有很多同学跟我写的一样,套用第一题的缩小条件:

  // 满足题目要求开始缩小窗口,并更新答案while (valid == need.size()) {char d = s.charAt(left);left++;if (need.containsKey(d)) {if (need.get(d) == have.get(d)) {valid--;// 在这里找答案,因为一定能保证left为开始索引ans.add(left - 1);}have.put(d, have.get(d) - 1);}}

仔细想想,这两题的区别是什么?第一题是只要一个子串内覆盖要求的字符数即可,而这题还有上面的题,是要求子串完全匹配目标串,不是要你找一个区间覆盖目标串中的字符了,而是必须所有字符种类+个数完全匹配,这就不一样了啊,第二、三题,由于要匹配目标串的字符种数+个数,所以,窗口缩小条件就变成了窗口的大小 >= 目标串的大小,因为长度必须匹配,当大小 >= 目标串大小时,肯定有结果了,无非就是找到了、没有找到,没有找到就赶紧左移,让窗口往后滑动,继续寻找;找到了,那就可以记录或者返回答案了。

一定要根据题目要求去写窗口缩小条件!

class Solution {public List<Integer> findAnagrams(String s, String p) {List<Integer> ans = new LinkedList<>();  // 记录答案HashMap<Character, Integer> need = new HashMap<>();HashMap<Character, Integer> have = new HashMap<>();// 统计需要字符数for (int i = 0; i < p.length(); i++) {char c = p.charAt(i);need.put(c, need.getOrDefault(c, 0) + 1);}int left = 0, right = 0;int valid = 0;while (right < s.length()) {// 扩大窗口char c = s.charAt(right);right++;if (need.containsKey(c)) {have.put(c, have.getOrDefault(c, 0) + 1);if (need.get(c).equals(have.get(c))) {valid++;}}// 满足题目要求开始缩小窗口,并更新答案// ※注意这里的缩小条件while (right - left >= p.length()) {if (valid == need.size()) {// 找到了答案ans.add(left);}// 不管找没找到都要缩小窗口char d = s.charAt(left);left++;if (need.containsKey(d)) {if (have.get(d).equals(need.get(d))) {valid--;}have.put(d, have.get(d) - 1);}}}return ans;}
}

3 无重复字符的最长子串(中等)

注意窗口缩小条件 + 寻找答案的时机:

class Solution {public int lengthOfLongestSubstring(String s) {// 没有目标子串了,那就只用维护窗口中的字符HashMap<Character, Integer> have = new HashMap<>();int left = 0;int right = 0;int ans = 0;while (right < s.length()) {char c = s.charAt(right);right++;have.put(c, have.getOrDefault(c, 0) + 1);while (have.get(c) > 1) {// 可以开始收缩了char d = s.charAt(left);left++;have.put(d, have.get(d) - 1);}// 这里更新答案,在这里说明肯定是不重复的// 注意上面right+1了,所以这里求长度不用+1if (right - left > ans) ans = right - left;}return ans;}
}

187 重复的DNA序列(中等)

重新梳理下题目含义:题目的意思是编写一个函数来查找子串,这个子串长度为10,在原字符串中出现超过一次。

用substring函数代替窗口的维护,i的移动来代表窗口的移动

class Solution {public List<String> findRepeatedDnaSequences(String s) {List<String> ans = new ArrayList<>();int n = s.length();Map<String, Integer> map = new HashMap<>();for (int i = 0; i + 10 <= n; i++) {String cur = s.substring(i, i + 10);int cnt = map.getOrDefault(cur, 0);if (cnt == 1) ans.add(cur);map.put(cur, cnt + 1);}return ans;}
}

总结滑动窗口无非下面三个点:

1、什么时候扩大窗口
2、什么时候缩小窗口
3、什么时候更新/寻找答案

二分

二分是一个经典算法,可以作为很多算法的基础,同时有很多场景、应用题,暗藏二分,有些可能需要二分题目数据,而有些则是二分答案,变化很多,当遇到一些题目没有头绪思路时,不妨试试二分!

    // 传入的数组必须是升序// 递归实现二分查找static int binarySearch(int[] arr, int low, int high, int key){if(low > high){// 没找到return -1;}int mid = (high + low) / 2;int midVal = arr[mid];if(midVal < key){return binarySearch(arr, mid+1, high, key);}else if(midVal > key){return binarySearch(arr, low, mid-1, key);}else{// 找到了return mid;}}// 迭代实现二分查找// 注意这个二分查找,找到的是最后一个满足条件的数,而不是第一个!!static int binarySearch1(int[] arr, int key){int begin = 0;int end = arr.length - 1;int mid;while(begin <= end){mid = (begin + end) / 2;if(arr[mid] == key){return mid;}else if(arr[mid] > key){end = mid - 1;}else if(arr[mid] < key){begin = mid + 1;}}// 没找到return -1;}// 迭代实现二分查找:第一个大于等于key的数下标,找不到就返回数组最后一个元素的下一个下标static int lowerbound(int[] arr, int key){int begin = 0;int end = arr.length;int mid;while(begin < end){mid = (begin + end) / 2;if(arr[mid] >= key){end = mid;}else{begin = mid + 1;}}return begin;}// 迭代实现二分查找:第一个大于key的数的下标,找不到就返回数组最后一个元素的下一个下标// 也就是数组长度static int upperbound(int[] arr, int key){int begin = 0;int end = arr.length;int mid;while(begin < end){mid = (begin + end) / 2;if(arr[mid] > key){end = mid;}else{begin = mid + 1;}}return begin;}

当left = 0,right = len - 1时,代表每次查找的区间都是左闭右闭所以,每次更新区间时,也要保证左右是闭,所以才需要left = mid + 1 和 right = mid - 1

当left = 0,right = len时,代表每次查找的区间都是左闭右开所以每次更新区间时,都要保证是左闭右开,所以只需要left = mid + 1即可,让right = mid就行,因为是右开,自动取到mid - 1

【经典面试题】※34 在排序数组中查找元素的第一个和最后一个位置(中等)

就是利用上面的模板去找就行了,只不过要特判一下结果是否满足题意

class Solution {public int[] searchRange(int[] nums, int target) {// 数组已经排过序// 找到第一个大于等于target的idx1// 找到第一个大于target的idx2// 最后答案区间:[idx1, idx2 - 1],当然需要idx1存在,否则idx2也不会存在int len = nums.length;int idx1 = search1(nums, target);if (idx1 == len || nums[idx1] != target) {// 没找到:两种情况return new int[] {-1, -1};}int idx2 = search2(nums, target);return new int[] {idx1, idx2 - 1};}// 找第一个大于等于target的idx,如果没有,返回数组长度public int search1(int[] nums, int target) {int left = 0;int right = nums.length;int mid;// 采用的是左闭右开的写法while (left < right) {// 避免溢出mid = left + (right - left) / 2;if (nums[mid] >= target) {right = mid;  // 右开} else {left = mid + 1;  // 左闭 }}// 最后答案都是返回leftreturn left;}// 找第一个大于target的idx,如果没有,返回数组长度public int search2(int[] nums, int target) {int left = 0;int right = nums.length;int mid;// 同样采用左闭右开while (left < right) {mid = left + (right - left) / 2;if (nums[mid] > target) {right = mid;  // 右开} else {left = mid + 1;  // 左闭}}// 最后答案都是leftreturn left;}
}

更多二分题目练习

【经典面试题】33 搜索旋转排序数组(中等)

class Solution {public int search(int[] nums, int target) {// 给你 旋转后 的数组 nums 和一个整数 target// 如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1int left = 0;int right = nums.length - 1;int mid;// 先判断前后哪部分有序while (left <= right) {mid = left + (right - left) / 2;// 找到结果下标if (nums[mid] == target) {return mid;}// 判断哪部分有序就在哪部分用二分if (nums[left] <= nums[mid]) {// 说明在左半部分,限制target的条件应该是mid + leftif (nums[left] <= target && target < nums[mid]) {// 说明right太大了right = mid - 1;} else {// 说明left太小了left = mid + 1;}} else {// 说明在右半部分,限制target的条件应该是mid + rightif (nums[mid] < target && target <= nums[right]) {// 说明left太小了left = mid + 1;} else {// 说明right太小了right = mid - 1;}}}// 没找到return -1;}
}

81 搜索旋转排序数组 II(中等)

相比上一题就多了一个重复数字的判断情况,如果数字重复了就移动left指针,去除重复项干扰,当然前后两道题都可以用同样的模板:

去除重复项的实质是为了构造出二分性,同时减少可能的结果集,当然这里由于使用了left++,只能说是局部的二分

class Solution {public boolean search(int[] nums, int target) {// 相比于原先的题目,少了一个限制条件:数组中的值可能重复int left = 0;int right = nums.length - 1;int mid;while (left <= right) {mid = left + (right - left) / 2;if (nums[mid] == target) {return true;}// 无法确定有序区间,就让left移动一下,方便确定有序区间if (nums[left] == nums[mid]) {left++;continue;}// 判断有序区间if (nums[left] <= nums[mid]) {// 说明在左半部分if (target >= nums[left] && target < nums[mid]) {// target < nums[mid],说明right大了right = mid - 1;} else {left = mid + 1;}} else {// 说明在右半部分if (target > nums[mid] && target <= nums[right]) {// target > nums[mid],说明left小了left = mid + 1;} else {right = mid - 1;}}}return false;}
}

4 寻找两个正序数组的中位数(困难)

这道题是CodeTop上看到的二分热门题,比较复杂,先放到这里哈哈哈哈

380 O(1) 时间插入、删除和获取随机元素(中等)

这道题要求实现一个类,满足插入、删除和获取随机元素操作的平均时间复杂度为 O(1)O(1)。

变长数组可以在 O(1)O(1) 的时间内完成获取随机元素操作,但是由于无法在 O(1)O(1) 的时间内判断元素是否存在,因此不能在 O(1)O(1) 的时间内完成插入和删除操作。哈希表可以在 O(1)O(1) 的时间内完成插入和删除操作,但是由于无法根据下标定位到特定元素,因此不能在 O(1)O(1) 的时间内完成获取随机元素操作。为了满足插入、删除和获取随机元素操作的时间复杂度都是 O(1)O(1),需要将变长数组和哈希表结合,变长数组中存储元素,哈希表中存储每个元素在变长数组中的下标。

本质是:哈希表 + 数组的结合,数组紧凑存放元素才能保证是等概率返回随即结果

class RandomizedSet {List<Integer> nums;Map<Integer, Integer> indices;Random random;public RandomizedSet() {nums = new ArrayList<Integer>();indices = new HashMap<Integer, Integer>();random = new Random();}public boolean insert(int val) {if (indices.containsKey(val)) {return false;}int index = nums.size();nums.add(val);indices.put(val, index);return true;}// 为了保证数组的紧凑,将最后一个元素拿来替换掉被删除的元素// 同时更新哈希表中的元素索引public boolean remove(int val) {if (!indices.containsKey(val)) {return false;}int index = indices.get(val);int last = nums.get(nums.size() - 1);nums.set(index, last);indices.put(last, index);nums.remove(nums.size() - 1);indices.remove(val);return true;}public int getRandom() {int randomIndex = random.nextInt(nums.size());return nums.get(randomIndex);}
}

【我想找一份实习】算法篇相关推荐

  1. 2讲个笑话:我想找一份理想的工作

    晚上,上完了线上课,忽然想起来还没吃饭,随手拿起桌子上的葡萄放在嘴里,边寻思着晚饭吃什么,边去洗脸让驱除一下上课的疲惫.热水滑过脸颊,卫生间雾气开始升腾起来,忽然想起下午咨询的女孩,也许此刻她在像火柴 ...

  2. 2讲个笑话:我想找一份更好的工作

    晚上,上完了线上课,忽然想起来还没吃饭,随手拿起桌子上的葡萄放在嘴里,边寻思着晚饭吃什么,边去洗脸让驱除一下上课的疲惫.热水滑过脸颊,卫生间雾气开始升腾起来,忽然想起下午咨询的女孩,也许此刻她在像火柴 ...

  3. 要不要出去找一份实习?

    关注.星标公众号,直达精彩内容 大家好,我是咸鱼君,二本出身,目前是一名电子电气工程专业在读的海外硕士,能力不高,项目较少.应妮姐和mo姐的邀请来分享一下我的实习经历. 我是19年的12月初放假回国的 ...

  4. 想找工作,这一篇15w字数+的文章帮你解决

    文章目录 前言 一 专业技能 1. 熟悉GoLang语言 1.1 Slice 1.2 Map 1.3 Channel 1.4 Goroutine 1.5 GMP调度 1.6 垃圾回收机制 1.7 其他 ...

  5. 海天食品的java开发工作如何_再三个月就秋招了,我想找一份java开发工作,现在应该怎么准备一下?...

    在找工作之前,大家都要做一些准备工作,java开发也是如此 掌握核心JavaSE 首先,从核心Java(JavaSE)开始学习,尽可能地掌握它.你应该了解和掌握一些基本概念,如循环,数组,运算符等等. ...

  6. 关于计算机安全文章,想找一份关于计算机安全的文章

    计算机学术论文是学术论文的一种.计算机学术论文的一般格局是: (一)题目.题目是论文的窗户,它应是论文内容的高度概括.好的论文题目能大体反映出作者研究的方向.成果.内容.意义.题目引用语要确切.简洁. ...

  7. 我想找一份python后端开发工作,需要具备哪些技能。

    作为一名 Python 后端开发人员,您应该具备以下技能: 熟练掌握 Python 编程语言 理解 Web 开发技术,如 HTTP 协议.RESTful API 等 经验丰富的数据库编程,比如 MyS ...

  8. 友情提示,你该找一份假期实习啦!

    假期已至, 这么漫长的寒假, 你是否有找一份实习的打算呢? 是否毫无头绪和思路? 是找一份毫无意义的推销工作,传单,快餐店,电话推销等,还是真正找到兴趣所在,专业相关,今天为大家分享一下. 实习渠道 ...

  9. 计算机大三如何找一份“好”实习呢?

    计算机大三了,除了毕业设计之外,最让学生们发愁的是找实习,因为学校将实习与毕业学分挂钩,抛开以上原因,还有就是为了好就业,实习在所难免.那么计算机大三如何找到一份"好"实习呢? 对 ...

最新文章

  1. 语义分割领域开山之作:Google提出用神经网络搜索实现语义分割
  2. LeetCode-Unique Binary Search Trees
  3. idea在Mybatis的xml里面写sql时,表名、字段、报红问题的解决方法
  4. php连接mysql总结_php连接数据库的三种方式的总结
  5. [react] 简要描述下你知道的react工作原理是什么?
  6. Azure Services Platform
  7. 7-192 素因子分解 (20 分)
  8. advice 和 拦截器_ControllerAdvice拦截器
  9. springboot + vue_Springboot+VUE---实现简单的websocket
  10. 安卓 qemu 运行linux,在Qemu的beagleboard上运行Android
  11. Git学习的简单笔记
  12. 顶级期刊:关于提升人体免疫力的一切!5点有益建议
  13. 大数据的核心价值是什么,主要体现在哪几方面?
  14. win2003的密钥
  15. Segmentation-Based Deep-Learning Approach for Surface-Defect Detection-论文阅读笔记
  16. stm32中的或运算 ||
  17. 浏览器阻挡cookies_解决WordPress登录出现Cookies被阻止或者您的浏览器不支持
  18. C++ 计算直线的交点数(动态规划)
  19. 陆军计算机ip等级,作战计算是科学不是技能,重点领域发布!
  20. 现代密码学 | 02:流密码——2

热门文章

  1. 封魔录聊天窗口默认文本功能
  2. 扫描图像智能定位增强美化处理黑白处理
  3. [转]关于内存地址的个人理解和操作原理
  4. 关于overflow:hidden溢出隐藏
  5. Java File类型文件无法删除(可能的解决办法)
  6. vb.net 导出为excel及邮件群发
  7. nginx配置fcgi
  8. git远程仓库分支的各命令的具体解析(git remote add)
  9. [分支限界]给定一个矩阵m*n,从左上角开始每次只能向右或者向下走,最后到右下角的位置共有多少种路径
  10. Windows Server2016 本地用户管理