目录(序号为leetcode题号)
202. 快乐数
编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」定义为:
- 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
- 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
- 如果 可以变为 1,那么这个数就是快乐数。
- 如果 n 是快乐数就返回 true ;不是,则返回 false 。
方法一:哈希表
解题思路:每次将平方和记录在哈希表中,如果该平方和再次出现,说明存在循环,变不到1,不是快乐数。
boolean isHappy(int n) {
int nums;
HashSet<Integer> set = new HashSet<>();
while(n != 1){
nums = 0;
while(n != 0){
int x = n%10;
n /= 10;
nums += x*x;
}
if(set.contains(nums)){
return false;
}
set.add(nums);
n = nums;
}
return true;
}
方法二:快慢指针
解题思路:如果存在循环,慢指针一定会追上快指针。
int getNext(int n){
int next = 0;
while(n != 0){
int x = n%10;
n = n/10;
next += x*x;
}
return next;
}
boolean isHappy(int n) {
int fast,slow;
slow = n;
fast = getNext(n);
while(fast != slow){
fast = getNext(getNext(fast));
slow = getNext(slow);
}
if(fast == 1){
return true;
}
return false;
}
204. 计数质数
统计所有小于非负整数 n 的质数的数量。
输入:n = 10
输出:4
解释:小于 10 的质数一共有 4 个, 它们是 2, 3, 5, 7 。
方法一:暴力枚举
超时了
boolean isPrime(int n){
for(int i = 2;i <= Math.sqrt(n);i++){
if(n % i == 0){
return false;
}
}
return true;
}
public int countPrimes(int n) {
int cut = 0;
for(int i = 2;i < n;i++){
if(isPrime(i)){
cut++;
}
}
return cut;
}
方法二:埃氏筛
如果一个数是质数,那么它的倍数一定不是质数。
使用一个数组,来记录标记。如果x是质数,则将 2x,3x…标记为非质数。实际上,不用从 2x开始标记,因为 2x,3x… 这些数一定在 x之前就被其他数的倍数标记过了,从 x*x标记即可。
public int countPrimes(int n) {
int cut = 0;
// 标记数组
int[] primes = new int[n];
for(int i = 2;i < n;i++){
if(primes[i] == 0){
cut += 1;
if((long)i * i < n){
// i是质数,从i*i开始标记。
for(int j = i * i;j < n;j += i){
primes[j] = 1;
}
}
}
}
return cut;
}
206. 反转链表
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
示例:
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
解题思路:链表的头插入,将链表反转。
public ListNode reverseList(ListNode head) {
ListNode reversal = new ListNode();
ListNode p;
while(head != null){
p = head.next;
head.next = reversal.next;
reversal.next = head;
head = p;
}
return reversal.next;
}
207. 课程表
你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。
在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,
其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程 bi 。
例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。
请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。
- 1 <= numCourses <= 105
- 0 <= prerequisites.length <= 5000
- prerequisites[i].length == 2
- 0 <= ai, bi < numCourses
- prerequisites[i] 中的所有课程对互不相同
示例:
输入:numCourses = 2, prerequisites = [[1,0]]
输出:true
解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。
解题思路:拓扑排序。
public boolean canFinish(int numCourses, int[][] prerequisites) {
// 边集,记录课程 i 的所有后续的课
List<List<Integer>> edges;
edges = new ArrayList<List<Integer>>();
for (int i = 0; i < numCourses; ++i) {
edges.add(new ArrayList<Integer>());
}
for (int[] info : prerequisites) {
edges.get(info[1]).add(info[0]);
}
// 记录入度的数组
int[] indegree = new int[numCourses];
int cut = 0;
for(int i = 0;i < prerequisites.length;i++){
indegree[prerequisites[i][0]]++;
}
// 将入度为 0 的课程入队列
Queue<Integer> queue = new LinkedList<>();
for(int i = 0;i < numCourses;i++){
if(indegree[i] == 0){
queue.add(i);
cut++;
}
}
while(!queue.isEmpty()){
int course = queue.remove();
// 取出所有course课程的后续课
for (int v: edges.get(course)) {
// 后续课的入度--
indegree[v]--;
// 如果 课程的入度为 0,则可以直接修本课程
if (indegree[v] == 0) {
queue.add(v);
cut++;
}
}
}
return cut == numCourses;
}
208. 实现 Trie (前缀树)
Trie(发音类似 “try”)或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。
请你实现 Trie 类:
- Trie() 初始化前缀树对象。
- void insert(String word) 向前缀树中插入字符串 word 。
- boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false 。
- boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false 。
示例:
输入
["Trie", "insert", "search", "search", "startsWith", "insert", "search"]
[[], ["apple"], ["apple"], ["app"], ["app"], ["app"], ["app"]]
输出
[null, null, true, false, true, null, true]
解释
Trie trie = new Trie();
trie.insert("apple");
trie.search("apple"); // 返回 True
trie.search("app"); // 返回 False
trie.startsWith("app"); // 返回 True
trie.insert("app");
trie.search("app"); // 返回 True
- 1 <= word.length, prefix.length <= 2000
- word 和 prefix 仅由小写英文字母组成
- insert、search 和 startsWith 调用次数 总计 不超过 3 * 104 次
解题思路:树。每个树节点有26个子节点,代表26个字母,每个节点有一个flag标志,如果flag标志为true,表示存在从根节点到当前节点的一个单词。
// 树的节点
class Node {
boolean flag;
Node[] next;
public Node(){
next = new Node[26];
}
}
// 字典数
class Trie {
Node root;
Node node;
public Trie() {
root = new Node();
}
public void insert(String word) {
char[] chars = word.toCharArray();
node = root;
for(int i = 0;i<word.length();i++){
int index = chars[i]-'a';
if(node.next[index] == null){
node.next[index] = new Node();
}
node = node.next[index];
}
node.flag = true;
}
public boolean search(String word) {
char[] chars = word.toCharArray();
node = root;
for(int i = 0;i < word.length();i++){
int index = chars[i]-'a';
if(node.next[index] == null){
return false;
}
node = node.next[index];
}
return node.flag;
}
public boolean startsWith(String prefix) {
char[] chars = prefix.toCharArray();
node = root;
for(int i = 0;i < prefix.length();i++){
int index = chars[i]-'a';
if(node.next[index] == null){
return false;
}
node = node.next[index];
}
return true;
}
}
210. 课程表 II
现在你总共有 numCourses 门课需要选,记为 0 到 numCourses - 1。给你一个数组 prerequisites ,其中 prerequisites[i] = [ai, bi] ,表示在选修课程 ai 前 必须 先选修 bi 。
例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示:[0,1] 。
返回你为了学完所有课程所安排的学习顺序。可能会有多个正确的顺序,你只要返回 任意一种 就可以了。如果不可能完成所有课程,返回 一个空数组 。
解题思路:在课程表I的基础上,增加一个数组,记录顺序即可。
public int[] findOrder(int numCourses, int[][] prerequisites) {
// 边集,记录课程 i 的所有后续的课
List<List<Integer>> edges;
// order,记录课程的顺序
int[] order = new int[numCourses];
edges = new ArrayList<List<Integer>>();
for (int i = 0; i < numCourses; ++i) {
edges.add(new ArrayList<Integer>());
}
for (int[] info : prerequisites) {
edges.get(info[1]).add(info[0]);
}
// 记录入度的数组
int[] indegree = new int[numCourses];
int cut = 0;
for (int i = 0; i < prerequisites.length; i++) {
indegree[prerequisites[i][0]]++;
}
// 将入度为 0 的课程入队列
Queue<Integer> queue = new LinkedList<>();
for (int i = 0; i < numCourses; i++) {
if (indegree[i] == 0) {
queue.add(i);
order[cut++] = i;
}
}
while (!queue.isEmpty()) {
int course = queue.remove();
// 取出所有course课程的后续课
for (int v : edges.get(course)) {
// 后续课的入度--
indegree[v]--;
// 如果 课程的入度为 0,则可以直接修本课程
if (indegree[v] == 0) {
queue.add(v);
order[cut++] = v;
}
}
}
if (cut == numCourses) {
return order;
}
return new int[]{};
}
212. 单词搜索 II
给定一个 m x n 二维字符网格 board 和一个单词(字符串)列表 words,找出所有同时在二维网格和字典中出现的单词。
单词必须按照字母顺序,通过 相邻的单元格 内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母在一个单词中不允许被重复使用。
示例:
输入:board = [["o","a","a","n"],["e","t","a","e"],
["i","h","k","r"],["i","f","l","v"]],
words = ["oath","pea","eat","rain"]
输出:["eat","oath"]
方法一:回溯
解题思路:采用回溯法,与 79题解法一样,不同的是判断多个单词
public List<String> findWords(char[][] board, String[] words) {
int m, n;
m = board.length;
n = board[0].length;
// flag 标记单元格是否被访问
boolean[][] flag = new boolean[board.length][board[0].length];
// 记录结果集
List<String> result = new ArrayList<>();
// 循环 k次,判断所有的单词是否能找到
for (int k = 0; k < words.length; k++) {
// 将String类型转换为 char,方便操作
char[] chars = words[k].toCharArray();
// 对所有单元格进行遍历
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
// 找到与首字母相同的单元格,进行回溯
if (board[i][j] == chars[0]) {
// 标记当前单元格被访问过
flag[i][j] = true;
// 为返回 true,则可以在网格中找到单词
if (backtrack(board, m, n, i, j, chars, chars.length, flag, 1)){
// 加入结果集,结束循环
result.add(words[k]);
break;
}
flag[i][j] = false;
}
}
// 判断是否找到,找到结束后面循环
if(result.contains(words[k])){
break;
}
}
// 将标记数组重新设置,继续寻找下一个单词。
for(int i = 0;i<m;i++){
Arrays.fill(flag[i],false);
}
}
return result;
}
// 参数说明,m,n,为网格的大小,i,j为当前网格的位置,
// word为当前单母,wlen字母数量,flag标记是否已经访问,num是当前单词的第几个字母
public boolean backtrack(char[][] board,int m,int n,int i, int j,char[] word,int wlen,boolean[][] flag,int num){
if(num == wlen) {
return true;
}
// 上下左右四个方向寻找匹配的值,未访问过的,继续回溯。
// 上
if(i - 1>= 0 && word[num] == board[i-1][j] && !flag[i-1][j]){
flag[i-1][j] = true;
if(backtrack(board,m,n,i-1,j,word,wlen,flag,num+1))
return true;
flag[i-1][j] = false;
}
// 下
if(i+1<m && word[num] == board[i+1][j] && !flag[i+1][j]){
flag[i+1][j] = true;
if(backtrack(board,m,n,i+1,j,word,wlen,flag,num+1))
return true;
flag[i+1][j] = false;
}
//左
if(j-1>=0 && word[num] == board[i][j-1] && !flag[i][j-1]){
flag[i][j-1] = true;
if(backtrack(board,m,n,i,j-1,word,wlen,flag,num+1))
return true;
flag[i][j-1] = false;
}
//右
if(j+1<n && word[num] == board[i][j+1] && !flag[i][j+1]){
flag[i][j+1] = true;
if(backtrack(board,m,n,i,j+1,word,wlen,flag,num+1))
return true;
flag[i][j+1] = false;
}
return false;
}
方法二:回溯+字典树
前缀树(字典树)是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。
前缀树可以用 O(∣S∣) 的时间复杂度完成如下操作,其中∣S∣ 是插入字符串或查询前缀的长度:
向前缀树中插入字符串 word;
查询前缀串 prefix 是否为已经插入到前缀树中的任意一个字符串 \textit{word}word 的前缀;
我们每遍历一个字符,判断其是否在前缀树当前层,如果在,就向上下左右扩散,同时,前缀树也向下移动一层。
需要回溯遍历整个board,对于每一个遇到的字符,都要判断其是否在前缀树中,同时,不在前缀树中的字符,直接剪枝掉即可。
优化:当我们在board中找到了某个单词,就把它从前缀树中删除
class Solution {
// 上下左右移动的方向
int[][] dirs = new int[][]{{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
public List<String> findWords(char[][] board, String[] words) {
// 结果集
List<String> resultList = new ArrayList<>();
// 构建字典树
TrieNode root = buildTrie(words);
int m = board.length;
int n = board[0].length;
// 记录某个下标是否访问过
boolean[][] visited = new boolean[m][n];
// 记录沿途遍历到的元素
StringBuilder result = new StringBuilder();
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[i].length; j++) {
// 从每个元素开始遍历
dfs(resultList, result, board, i, j, root, root, visited);
}
}
// 题目要求返回List
return resultList;
}
private void dfs(List<String> resultList, StringBuilder result, char[][] board,
int i, int j, TrieNode root, TrieNode node, boolean[][] visited) {
// 判断越界,或者访问过,或者不在字典树中,直接返回
if (i < 0 || j < 0 || i >= board.length || j >= board[0].length || visited[i][j]
|| node == null || node.children[board[i][j] - 'a'] == null) {
return;
}
// 记录当前字符
result.append(board[i][j]);
// 如果有结束字符,加入结果集中
if (node.children[board[i][j] - 'a'].isEnd) {
String word = result.toString();
resultList.add(word);
deleteWordFromTrie(root, word);
}
// 记录当前元素已访问
visited[i][j] = true;
// 按四个方向去遍历
for (int[] dir : dirs) {
dfs(resultList, result, board, i + dir[0], j + dir[1], root, node.children[board[i][j] - 'a'], visited);
}
// 还原状态
visited[i][j] = false;
result.deleteCharAt(result.length() - 1);
}
private void deleteWordFromTrie(TrieNode root, String word) {
// 删除并没有那么好搞,需要先找到最后一个字符,从下往上删除
delete(root, word, 0);
}
// 返回true表示可以把沿途节点删除,返回false表示不能删除沿途节点
private boolean delete(TrieNode prev, String word, int i) {
if (i == word.length() - 1) {
// 如果后面还有单词则不能直接删除,比如dog和dogs是在一条链上,删除dog的时候不能把整个链删除了
TrieNode curr = prev.children[word.charAt(i) - 'a'];
if (hasChildren(curr)) {
curr.isEnd = false;
return false;
} else {
prev.children[word.charAt(i) - 'a'] = null;
return true;
}
} else {
// 如果后面的说可以删除,并且当前节点不是单词节点,并且没有其它子节点了,那么删除之,否则返回false
// 比如删除dogs的时候不能把dog删除了
// 比如同时存在dog和dig两个单词,删除dog的时候返回到d的时候,这个d是不能删除的
if (delete(prev.children[word.charAt(i) - 'a'], word, i + 1)
&& !prev.children[word.charAt(i) - 'a'].isEnd
&& !hasChildren(prev.children[word.charAt(i) - 'a'])) {
prev.children[word.charAt(i) - 'a'] = null;
return true;
}
return false;
}
}
private boolean hasChildren(TrieNode curr) {
for (TrieNode child : curr.children) {
if (child != null) {
return true;
}
}
return false;
}
private TrieNode buildTrie(String[] words) {
TrieNode root = new TrieNode();
for (String word : words) {
char[] arr = word.toCharArray();
TrieNode curr = root;
for (char c : arr) {
if (curr.children[c - 'a'] == null) {
curr.children[c - 'a'] = new TrieNode();
}
curr = curr.children[c - 'a'];
}
curr.isEnd = true;
}
return root;
}
class TrieNode {
// 记录到这个节点是否是一个完整的单词
boolean isEnd = false;
// 孩子节点,题目说了都是小写字母,所以用数组,否则可以用HashMap替换
TrieNode[] children = new TrieNode[26];
}
}
215. 数组中的第K个最大元素
给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。
请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
输出: 4
提示:
- 1 <= k <= nums.length <= 104
- -104 <= nums[i] <= 104
解题思路:使用快速选择,快速排序中确定一个轴值,将小于轴值的放在左边,大于轴值的放在右边,然后判断轴值位置 和 k 的位置,轴值位置 小于k的位置,则在轴值右半边进行快排,大于k的位置,则在左半边进行快排,直到轴值位置等于 k的位置,返回。
public int findKthLargest(int[] nums, int k) {
quickSelect(nums,0,nums.length - 1,nums.length - k);
return nums[nums.length - k];
}
void swap(int[] nums,int i,int j){
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
void quickSelect(int[] nums, int start, int end, int k){
int i = start - 1;
int j = end;
int m = (int)(Math.random() * (end - start) + start);
swap(nums,end,m);
m = end;
while(true){
while (i < j && nums[++i] < nums[m]);
while(i < j && nums[--j] > nums[m]);
if(i < j){
swap(nums,i,j);
}else{
break;
}
}
swap(nums,i,end);
if(i == k){
return;
} else if(i > k){
quickSelect(nums,start,i - 1,k);
} else {
quickSelect(nums,i + 1,end,k);
}
}
217. 存在重复元素
给定一个整数数组,判断是否存在重复元素。
如果存在一值在数组中出现至少两次,函数返回 true 。如果数组中每个元素都不相同,则返回 false。
示例:
输入: [1,2,3,1]
输出: true
解题思路:对数组排序,然后判断相邻元素是否相等。还可以使用哈希表。
public boolean containsDuplicate(int[] nums) {
HashSet<Integer> set = new HashSet<>();
for(int i = 0;i<nums.length;i++){
if(!set.add(nums[i])){
return true;
}
}
return false;
}
218. 天际线问题
城市的天际线是从远处观看该城市中所有建筑物形成的轮廓的外部轮廓。给你所有建筑物的位置和高度,请返回由这些建筑物形成的 天际线 。
每个建筑物的几何信息由数组 buildings 表示,其中三元组 buildings[i] = [lefti, righti, heighti] 表示:
- lefti 是第 i 座建筑物左边缘的 x 坐标。
- righti 是第 i 座建筑物右边缘的 x 坐标。
- heighti 是第 i 座建筑物的高度。
天际线 应该表示为由 “关键点” 组成的列表,格式 [[x1,y1],[x2,y2],…] ,并按 x 坐标 进行 排序 。关键点是水平线段的左端点。列表中最后一个点是最右侧建筑物的终点,y 坐标始终为 0 ,仅用于标记天际线的终点。此外,任何两个相邻建筑物之间的地面都应被视为天际线轮廓的一部分。
注意:输出天际线中不得有连续的相同高度的水平线。例如 […[2 3], [4 5], [7 5], [11 5], [12 7]…] 是不正确的答案;三条高度为 5 的线应该在最终输出中合并为一个:[…[2 3], [4 5], [12 7], …]
输入:buildings = [[2,9,10],[3,7,15],[5,12,12],[15,20,10],[19,24,8]]
输出:[[2,10],[3,15],[7,12],[12,0],[15,10],[20,8],[24,0]]
解释:
图 A 显示输入的所有建筑物的位置和高度,
图 B 显示由这些建筑物形成的天际线。图 B 中的红点表示输出列表中的关键点。
- 1 <= buildings.length <= 104
- 0 <= lefti < righti <= 231 - 1
- 1 <= heighti <= 231 - 1
- buildings 按 lefti 非递减排序
方法一:扫描线 + 优先队列
解题思路:关键点的横坐标总是落在建筑的左右边缘上。这样我们可以只考虑每一座建筑的边缘作为横坐标,这样其对应的纵坐标为「包含该横坐标」的所有建筑的最大高度。
当关键点为某建筑的右边缘时,该建筑的高度对关键点的纵坐标是没有贡献的。
包含该横坐标的定义:建筑的左边缘小于等于该横坐标,右边缘大于该横坐标(也就是我们不考虑建筑的右边缘)。即对于包含横坐标 x 的建筑 i,有 x∈ [lefti,righti),不包含右边缘。
在部分情况下,「包含该横坐标」的建筑并不存在。例如当图中只有一座建筑时,该建筑的左右边缘均对应一个关键点,当横坐标为其右边缘时,这唯一的建筑对其纵坐标没有贡献。因此该横坐标对应的纵坐标的大小为 0。
暴力的算法:O(n)地枚举建筑的每一个边缘作为关键点的横坐标,过程中我们 O(n) 地检查每一座建筑是否「包含该横坐标」,找到最大高度,即为该关键点的纵坐标。该算法的时间复杂度是 O(n2),我们需要进行优化。
可以用优先队列来优化寻找最大高度的时间,在我们从左到右枚举横坐标的过程中,实时地更新该优先队列即可。这样无论何时,优先队列的队首元素即为最大高度。为了维护优先队列,我们需要使用「延迟删除」的技巧,即我们无需每次横坐标改变就立刻将优先队列中所有不符合条件的元素都删除,而只需要保证优先队列的队首元素「包含该横坐标」即可。
具体地,为了按顺序枚举横坐标,我们用数组 boundaries 保存所有的边缘,排序后遍历该数组即可。首先将「包含该横坐标」的建筑加入到优先队列中,然后不断检查优先队列的队首元素是否「包含该横坐标」,如果不「包含该横坐标」,我们就将该队首元素弹出队列,直到队空或队首元素「包含该横坐标」即可。最后我们用变量 maxn 记录最大高度(即纵坐标的值),当优先队列为空时,maxn=0,否则 maxn 即为队首元素。
最后我们还需要再做一步检查:如果当前关键点的纵坐标大小与前一个关键点的纵坐标大小相同,则说明当前关键点无效,我们跳过该关键点即可。
public List<List<Integer>> getSkyline(int[][] buildings) {
// 优先队列,按照高度排序
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> b[1] - a[1]);
// 将所有关键点,加入集合
List<Integer> boundaries = new ArrayList<>();
for (int[] building : buildings) {
boundaries.add(building[0]);
boundaries.add(building[1]);
}
// 从小到大进行排序。
Collections.sort(boundaries);
List<List<Integer>> ret = new ArrayList<>();
int n = buildings.length, idx = 0;
// 对每一个关键点进行考虑
for (int boundary : boundaries) {
// 将所有包含关键点的建筑加入队列
while (idx < n && buildings[idx][0] <= boundary && boundary < buildings[idx][1]) {
// 只需要将右侧端点和高度加入队列即可。
pq.offer(new int[]{buildings[idx][1], buildings[idx][2]});
idx++;
}
// 如果队列的首元素,不包含该关键点,则抛出队列
while (!pq.isEmpty() && pq.peek()[0] <= boundary) {
pq.poll();
}
// 此时,队列的首元素即为关键点的最大高度
int maxn = pq.isEmpty() ? 0 : pq.peek()[1];
// 如果最大高度与前一个结果的高度一样,则抛弃该关键点,否则,加入结果集。
if (ret.size() == 0 || maxn != ret.get(ret.size() - 1).get(1)) {
ret.add(Arrays.asList(boundary, maxn));
}
}
return ret;
}