目录
11.1 有效的数独 (中等):数组记录
11.2 矩阵置零(中等):使用标记数组
11.3 从中序遍历和后序遍历序列构造二叉树(中等):哈希表 + 递归
11.4 相交链表(简单):哈希集合
11.5 最长连续序列(中等):哈希表
11.6 复制带随机指针的链表(中等):哈希表 + 回溯
11.7 多数元素(简单):哈希表
11.8 克隆图(中等):深度优先搜索 + 哈希表
11.9 单词拆分(中等):动态规划 + 哈希集合
11.10 分数到小数(中等):哈希表 + 长除法
11.11 重复的DNA序列(中等):哈希表
11.12 快乐数(简单):哈希集合
11.13 哈希表总结!!!
题目:请你判断一个 9 x 9
的数独是否有效。只需要 根据以下规则 ,验证已经填入的数字是否有效即可。
数字 1-9
在每一行只能出现一次。
数字 1-9
在每一列只能出现一次。
数字 1-9
在每一个以粗实线分隔的 3x3
宫内只能出现一次。(请参考示例图)
思想:由于数独只能每行、每列、每个 3x3
宫内只能出现一次,因此我们设置三个数组,分别保存出现的数字信息:
使用line[i][x]
存储第i
行是否出现过x + 1
这个值
使用column[j][x]
存储第j
列是否出现过x + 1
这个值
使用seg[i / 3][j / 3][x]
存储第[i/3 ,j/3]
个位置是否出现过x + 1
这个值
注意:由于数组索引从0开始,而数字是 1-9
,因此需要对每次的x - 1
总结:寻求所有可能结果,首选回溯法
代码:
class Solution {
public boolean isValidSudoku(char[][] board) {
int[][] rows = new int[9][9];
int[][] columns = new int[9][9];
int[][][] subboxes = new int[3][3][9];
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
char c = board[i][j];
if (c != '.') {
int index = c - '0' - 1;
rows[i][index]++;
columns[j][index]++;
subboxes[i / 3][j / 3][index]++;
if (rows[i][index] > 1 || columns[j][index] > 1 || subboxes[i / 3][j / 3][index] > 1) {
return false;
}
}
}
}
return true;
}
}
题目:给定一个 m x n
的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。
思想:使用两个标记数组分别记录每一行每一列是否有零出现
遍历该数组,若某个元素为0,那么该元素所在的行和列所对应标记数组的位置为true
遍历标记数组,用标记数组更新原数组即可
总结:寻求所有可能结果,首选回溯法
代码:
class Solution {
public void setZeroes(int[][] matrix) {
//拿到数组的长与短
int m = matrix.length;
int n = matrix[0].length;
//定义两个标记数组
boolean[] row = new boolean[m];
boolean[] col = new boolean[n];
//若有值为 0,则将数组标记为true
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
if(matrix[i][j] == 0){
row[i] = true;
col[j] = true;
}
}
}
//若数组为true,则将这一行的值和这一列的值均置为true
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
if(row[i] || col[j]){
matrix[i][j] = 0;
}
}
}
}
}
题目:给定两个整数数组 inorder
和 postorder
,其中 inorder
是二叉树的中序遍历, postorder
是同一棵树的后序遍历,请你构造并返回这颗 二叉树 。
思想:后序遍历的最后一个值就是根节点,后序遍历的思想是:左子树、右子树、根节点,因此当我们从后向前取后序遍历数组时,应该先构造的是右子树,然后构造的是左子树
使用哈希表存储中序序列,从而高效的查询元素
定义递归函数help(in_left, in_right)
表示当前递归到中序遍历子树的左右边界,开始时为help(0, n - 1)
当in_left > in_right
,子树为空
查询后序遍历中最后一个节点为根节点,索引为index
,从in_left
到index - 1
属于左子树,从index + 1
到in_right
属于右子树
注意:后续遍历列表中的每次取最后一个数为根节点,因此每次先创建右子树节点,然后创建左子树(每次先被构造出来的为右子树)
总结:巧妙运用后序遍历的规律,找到节点之间的对应关系
代码:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
//创建几个变量,用于不同方法使用:当前索引位置、中序遍历数组、后序遍历数组
int post_index;
int[] inorder;
int[] postorder;
//创建一个Map数组,用来存放中序遍历的元素与对应索引
Map map = new HashMap<>();
public TreeNode buildTree(int[] inorder, int[] postorder) {
//将中序遍历的数组存到map中
int i = 0;
for(int v : inorder){
map.put(v, i++);
}
//为创建的变量赋值:从后续遍历的倒数第一个节点开始
post_index = postorder.length - 1;
this.inorder = inorder;
this.postorder = postorder;
return help(0, postorder.length - 1);
}
public TreeNode help(int in_left, int in_right){
//当左边界大于右边界,说明子树为空,直接返回null
if(in_left > in_right){
return null;
}
//得到根节点的值
int root_val = postorder[post_index];
//从后往前取后序遍历的值,将post_index--
post_index--;
//构造为树的根节点
TreeNode root = new TreeNode(root_val);
//取得根节点的索引,索引的左边都是左子树,右边都是右子树
int root_index = map.get(root_val);
//先构造右子树
root.right = help(root_index + 1, in_right);
//后构造左子树
root.left = help(in_left, root_index - 1);
return root;
}
}
题目:给你两个单链表的头节点 headA
和 headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null
。
思想:将其中一个链表存入哈希集合,遍历另一个链表,当遇到相同元素时返回即可
总结:运用哈希集合的不重复性
代码:
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
Set set = new HashSet<>();
while(headA != null){
set.add(headA);
headA = headA.next;
}
while(headB != null){
if(set.contains(headB)){
return headB;
}
headB = headB.next;
}
return null;
}
}
题目:给定一个未排序的整数数组 nums
,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。请你设计并实现时间复杂度为 O(n)
的算法解决此问题。
思想:为了满足时间复杂度的要求,使用一个哈希表来存储数组中的元素,此时已经进行了去重
一个剪枝操作:由于可能存在一个序列,已经是一个x,x+1,x+2,⋯ ,x+y
的连续序列,如果此时依旧从x + 1
处匹配,就会浪费过多的时间,因此我们进行剪枝:
要枚举的数一定不存在前驱数x - 1
,否则就应该从x - 1
匹配,而不是从x
匹配,因此将其跳过
总结:利用剪枝操作,满足题目的复杂度约束
代码:
class Solution {
public int longestConsecutive(int[] nums) {
if(nums == null || nums.length == 0){
return 0;
}
Set set = new HashSet<>();
for(int v : nums){
set.add(v);
}
int res = 0;
for(int num : set){
//进行剪枝操作
if(!set.contains(num - 1)){
int curr = num;
int currRes = 1;
//如果set中存在x + 1,curr++,并记录当前的结果
while(set.contains(curr + 1)){
curr++;
currRes++;
}
res = Math.max(res, currRes);
}
}
return res;
}
}
题目:给你一个长度为 n
的链表,每个节点包含一个额外增加的随机指针 random
,该指针可以指向链表中的任何节点或空节点。构造这个链表的 深拷贝。
思想:由于该链表存在一个存在一个随机指针,如果直接复制链表及其两个指针,随机指针指向的节点可能还未创建;因此我们利用回溯的方法,让每个节点的拷贝都独立完成
对当前节点进行拷贝,然后拷贝当前节点的next
和当前节点的随即指针指向的节点,最后将创建的节点返回
总结:利用map
快速的查看节点的拷贝情况
代码:
/*
// Definition for a Node.
class Node {
int val;
Node next;
Node random;
public Node(int val) {
this.val = val;
this.next = null;
this.random = null;
}
}
*/
class Solution {
//创建一个哈希表,存储当前指针和新指针的拷贝情况
Map map = new HashMap<>();
public Node copyRandomList(Node head) {
//若head为空则返回null
if(head == null){
return null;
}
//当前map中还未创建新指针时
if(!map.containsKey(head)){
Node newHead = new Node(head.val);
map.put(head, newHead);
//新节点的next 和 random就是head的next 和 random
newHead.next = copyRandomList(head.next);
newHead.random = copyRandomList(head.random);
}
return map.get(head);
}
}
题目:给定一个大小为 n
的数组 nums
,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋
的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
思想:使用哈希表存储每个元素出现的次数
总结:利用map
快速的查看节点的拷贝情况
代码:
class Solution {
public int majorityElement(int[] nums) {
//拿到每个元素出现的次数
Map map = count(nums);
//遍历数组,找到出现次数最多的元素
Map.Entry maxCountEntry = null;
for(Map.Entry entry : map.entrySet()){
//
if(maxCountEntry == null || entry.getValue() > maxCountEntry.getValue()){
maxCountEntry = entry;
}
}
return maxCountEntry.getKey();
}
//获取每个元素出现的次数
public Map count(int[] nums){
Map map = new HashMap<>();
//记录nums中每一个数出现的次数
for(int v : nums){
if(!map.containsKey(v)){
map.put(v, 1);
}else{
map.put(v, map.get(v) + 1);
}
}
return map;
}
}
题目:给你无向 连通 图中一个节点的引用,请你返回该图的 深拷贝(克隆)。图中的每个节点都包含它的值 val
(int
) 和其邻居的列表(list[Node]
)。
思想:从给定节点出发,进行图的遍历,并在遍历的过程中完成深拷贝
注意:由于给定的是无向边,因此两个节点可以相互指向,从而陷入死循环的问题,我们需要使用哈希表记录以及被克隆过的节点
使用哈希表存储已被访问和克隆的节点,哈希表的key
是原始图中的节点,value
是克隆图中的对应节点
从给定节点开始遍历图,若已经被访问过,则返回其克隆图中的对应节点
总结:利用map
快速的查看节点的拷贝情况
代码:
class Solution {
//创建一个哈希表,存储节点和克隆节点
Map map = new HashMap<>();
public Node cloneGraph(Node node) {
//若node未空,直接返回
if(node == null){
return null;
}
//每次拷贝之前,先判断是否已经拷贝过(拷贝过在进行拷贝就容易死循环);直接返回拷贝的值
if(map.containsKey(node)){
return map.get(node);
}
//若没有拷贝过:先拷贝节点值
Node cloneNode = new Node(node.val);
//将拷贝过的节点存入哈希表,避免重复拷贝
map.put(node, cloneNode);
//拷贝节点的邻居列表
for(Node neighbor : node.neighbors){
cloneNode.neighbors.add(cloneGraph(neighbor));
}
return cloneNode;
}
}
题目:给你一个字符串 s
和一个字符串列表 wordDict
作为字典。请你判断是否可以利用字典中出现的单词拼接出 s
。注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
思想:定义dp[i]
表示字符串s
的前i
个字符组成的字符串s[0,...,i - 1]
是否能被字典中若干个单词组成
则要判断dp[i]
,必须枚举s[0,...,i - 1]
,将s[0,...,i - 1]
按照分割点j
分为两部分,我们有:
dp[i] = dp[j] && 子串 s[j..i−1] 是否出现在字典中
,
用哈希集合存入字典,从而避免冗余操作
总结:利用动态规划求解前后逻辑相关的问题
代码:
class Solution {
public boolean wordBreak(String s, List wordDict) {
//将字典存入set中,去重
Set set = new HashSet<>(wordDict);
//设置dp,没有元素则返回true
boolean dp[] = new boolean[s.length() + 1];
dp[0] = true;
//dp[i]成立的条件是dp[j] && 字典中是否存在s[j,...i - 1]
for(int i = 1; i <= s.length(); i++){
//输入一个分割点,位于[0, i]之间
for(int j = 0; j < i; j++){
if(dp[j] && set.contains(s.substring(j, i))){
dp[i] = true;
break;
}
}
}
return dp[s.length()];
}
}
题目:给定两个整数,分别表示分数的分子 numerator
和分母 denominator
,以 字符串形式返回小数 。如果小数部分为循环小数,则将循环的部分括在括号内。如果存在多个答案,只需返回 任意一个 。对于所有给定的输入,保证 答案字符串的长度小于 10的四次方
思想:为了避免结果溢出,将输入的int
类型转换为long
类型
首先,判断结果的正负,若为负数,要加上-
其次,判断计算后的结果,有三种可能:
整数,根据正负直接返回
有限小数,计算小数值拼接上小数点.
返回
无限循环小数,找出循环部分加入括号
计算时,先计算整数部分,后根据余数计算小数部分
计算小数部分:每次将余数×10
,然后计算小数的下一位,得到新的余数,直到余数为0或者找到循环部分为止
在寻找循环部分时,使用哈希表存储每个余数在小数部分第一次出现的下标(字符串的此时的长度就是每次余数的下标)
注意:相同的余数计算得到的小数的下一位一定是相同的,如果计算中发现某一位的余数在之前出现过,就说明是循环部分
使用哈希表存储每个余数在小数部分第一次出现的下标
比如:假设存在下标 j
和k
,满足j ≤ k
且 remainderj=remainderk+1
,则小数部分的第k + 1
位和小数部分的第j
位相同,因此小数部分的第 j
位到第 k
位是一个循环节。在小数部分的第j
位之前加上左括号,在小数部分的末尾(即第 k
位之后)加上右括号
总结:理清除法中的所有逻辑,注意计算时的先后顺序
代码:
class Solution {
public String fractionToDecimal(int numerator, int denominator) {
//创建结果字符串,一般用StringBuffer
StringBuffer res = new StringBuffer();
//创建一个Map,用来存储键为每次计算结果的余数,值为当前余数插入的下标(也就是结果字符串的长度)
Map map = new HashMap<>();
//避免溢出的转换
long a = numerator, b = denominator;
//先判断结果的正负;判断完后,取二者的绝对值相除
if(a * b < 0) res.append("-");
a = Math.abs(a);
b = Math.abs(b);
res.append(a / b);
//如果结果为整数(没有余数),直接返回
if(a % b == 0) return res.toString();
//程序到这里就说明不为整数,此时需要加上小数点
res.append(".");
//计算结果为小数的情况: 注意,每次的余数乘以10再除分母;
//余数不为 0 且 未在map中出现过,说明还需要继续求解
while((a = (a % b) * 10) > 0 && !map.containsKey(a)){
//将余数加入Map
map.put(a, res.length());
res.append(a / b);
}
//程序走到这里,说明余数 a 为 0;或者出现了相同的余数
//如果余数为0,说明是有限小数,将其返回
if(a == 0) return res.toString();
//如果出现了相同的余数,则在a第一次出现的位置加入左括号,在最后出现的位置加入右括号
return res.insert(map.get(a).intValue(), "(").append(")").toString();
}
}
题目:DNA序列 由一系列核苷酸组成,缩写为 'A'
, 'C'
, 'G'
和 'T'
.。
例如,"ACGAATTCCG"
是一个 DNA序列 。
在研究 DNA 时,识别 DNA 中的重复序列非常有用。给定一个表示 DNA序列 的字符串 s
,返回所有在 DNA 分子中出现不止一次的 长度为 10
的序列(子字符串)。你可以按 任意顺序 返回答案。
思想:用一个哈希表统计s
中长度为10
的子串出现的次数,返回所有出现次数超过10
的子串
总结:利用哈希表的map.getOrDefault()
方法,记录子串出现过的此时
代码:
class Solution {
public List findRepeatedDnaSequences(String s) {
List res = new ArrayList<>();
Map map = new HashMap<>();
//将s中长度为10的子串存入即可
if(s.length() < 10){
return res;
}
//找到s中所有长度为10的子串,存入map中
for(int i = 0; i <= s.length() - 10; i++){
String sub = s.substring(i, i + 10);
//将s中每一个长度为10的子串存入map中,注意使用map.getOrDefault()方法,创建初始值
map.put(sub, map.getOrDefault(sub, 0) + 1);
if(map.get(sub) == 2){
res.add(sub);
}
}
return res;
}
}
题目:编写一个算法来判断一个数 n
是不是快乐数。
「快乐数」 定义为:
对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
如果这个过程 结果为 1,那么这个数就是快乐数。
如果 n
是 快乐数 就返回 true
;不是,则返回 false
。
思想:将该数进行数位分离,求平方和;
用一个哈希集合存储每次生成的平方和是否在哈希集合中,如果不在则添加,如果在就说明该数处于一个循环中,不可能返回1
,也就不是快乐数
总结:要知道数的分离平方只有三种情况:最终得到1
;进入一个循环;越来越大接近无穷大(不可能发生)。根据这几种情况求解,选择哈希集合判断是否出现过只需要O(1)
的复杂度
代码:
class Solution {
public boolean isHappy(int n) {
//存储生成的平方数
Set set = new HashSet<>();
while(n != 1 && !set.contains(n)){
set.add(n);
n = getNext(n);
}
return n == 1;
}
//获取每个位置的平方数
public int getNext(int n){
int res = 0;
//每次取余数,然后将其除10,得到每一个位置的数
while(n > 0){
int temp = n % 10;
n = n / 10;
res += temp * temp;
}
return res;
}
}
哈希表最大的好处在于寻找元素的时间复杂度仅为O(1)
,通常用来存入需要验证的数据,进而判断是否存在于哈希表内
哈希表的常用方法:
map.getOrDefault(key, new E())
map.containsKey(key), map.containsValue(value)
map.put(key, value)
map.values()
:获取所有的键值信息;Collection values = map.values();