数组
136. 只出现一次的数字
题目
思路
/**
思想: 注意本题说,任何数都出现两次,唯独一个数出现一次,找出这个只出现一次的数
注意异或^运算的几个思路:
1. 异或运算具有交换律
2. 两个相同的数异或为0
3. 0与任何数异或等于任何数
所以 我们要保证时间复杂度为线性,空间复杂度为原地算法
只需将所有数都异或,两两出现的数异或为0,0与单独的那个数异或还是单独的那个数
*/
代码
class Solution {
public int singleNumber(int[] nums) {
/**
思想: 注意本题说,任何数都出现两次,唯独一个数出现一次,找出这个只出现一次的数
注意异或^运算的几个思路:
1. 异或运算具有交换律
2. 两个相同的数异或为0
3. 0与任何数异或等于任何数
所以 我们要保证时间复杂度为线性,空间复杂度为原地算法
只需将所有数都异或,两两出现的数异或为0,0与单独的那个数异或还是单独的那个数
*/
int ans = nums[0];
for(int i=1; i<nums.length; i++) {
ans = ans^nums[i];
}
return ans;
}
}
169. 多数元素
题目
思想
/**
思想: 通过哈希表实现
1. 先以数值为键,数字出现次数为值统计到哈希表中。
2. 再遍历哈希表找出最大的出现次数的数值(题目提到一定存在多数元素)
*/
代码
class Solution {
public int majorityElement(int[] nums) {
/**
思想: 通过哈希表实现
1. 先以数值为键,数字出现次数为值统计到哈希表中。
2. 再遍历哈希表找出最大的出现次数的数值(题目提到一定存在多数元素)
*/
// 构建统计数据出现次数的哈希表
Map<Integer, Integer> counts = new HashMap<>();
for(int num: nums) {
if(!counts.containsKey(num)) {
// 假如该数据不包括在哈希表中
counts.put(num, 1);
}else {
// 假如该数据包括在哈希表中
counts.put(num, counts.get(num)+1);
}
}
// 设置一个保留键值对的变量
Map.Entry<Integer,Integer> entryend = null;
// 遍历所有的键值对
for(Map.Entry<Integer, Integer> entry : counts.entrySet()) {
if(entryend == null || entry.getValue() > entryend.getValue()) {
// 第一对,或者有大于当前保存的键值对时更新
entryend = entry;
}
}
// 最后返回键名
return entryend.getKey();
}
}
15. 三数之和
题目
思想
/**
思想:
1. 先将数组从小到大排序
2. 假如数组的元素小于3时,直接返回[]
3. 因为数组已经为升序,所以第i个元素大于0时,再和后面的元素组和一定不会大于0了
4. 若当前元素和前一个元素相同了,也不要再用该元素作为最小的元素进行判断了(去重)
5. 除了上面的情况,就可以以当前第i个元素作为最小的元素,该元素后的子表记录头尾进行循环加判断找到为0的加和项
6. 同理,加和为0成立后,头尾指针要判断去重
7. 加和小于0.左指针右移动; 加和大于0,右指针左移动。
*/
代码
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
/**
思想:
1. 先将数组从小到大排序
2. 假如数组的元素小于3时,直接返回[]
3. 因为数组已经为升序,所以第i个元素大于0时,再和后面的元素组和一定不会大于0了
4. 若当前元素和前一个元素相同了,也不要再用该元素作为最小的元素进行判断了(去重)
5. 除了上面的情况,就可以以当前第i个元素作为最小的元素,该元素后的子表记录头尾进行循环加判断找到为0的加和项
6. 同理,加和为0成立后,头尾指针要判断去重
7. 加和小于0.左指针右移动; 加和大于0,右指针左移动。
*/
// 定义一个结果集
List<List<Integer>> res = new ArrayList<>();
// 保留数组长度
int len = nums.length;
// 假如当前数组为空或元素数小于直接返回空
if(nums == null || len<3) {
return res;
}
// 将数组排序
Arrays.sort(nums);
// 遍历数组中的每个元素,以遍历的值为最小值,看能否凑加和为0的三元组
for(int i=0; i<len; i++) {
// 若遍历的最小元素都大于0了,肯定后续就没有符合条件的三元组了
if(nums[i]>0) {
break;
}
// 用于判断去重,假如起始值和前面值相同,跳过本次操作
if(i>0 && nums[i] == nums[i-1]) continue;
int l = i+1;
int r = len-1;
// 当两个头尾指针不碰到时,循环继续
while(l<r) {
// 将三数相加
int sum = nums[i] + nums[l] + nums[r];
// 如果等于0,则将对应值加入到列表中
if(sum == 0) {
res.add(Arrays.asList(nums[i], nums[l], nums[r]));
// 去重,假如左指针有相同的直接右移
while(l<r && nums[l] == nums[l+1]) {
l++;
}
// 去重, 假如右指针有相同的直接左移
while(l<r && nums[r] == nums[r-1]) {
r--;
}
// 假如无重复,也要变一下左右指针,左指针右移动,右指针左移
l++;
r--;
}else if(sum<0) {
// 加和小于0时,左指针右移
l++;
}else {
// 加和大于0时,右指针左移
r--;
}
}
}
// 返回结果集
return res;
}
}
75. 颜色分类
问题
思想
/**
思想: (本题不可以用排序,假如拍 102这种形式的就无法搞定了)
设置两个指针一个指向头p0,一个指向尾部p2, 根据题目要求,排成012的格式
然后开始遍历数组,当遍历到0时,与p0交换,并且p0指针后移
遍历到2时,与p2指针交换
但是要判断换到当前i位置的数是不是1,不为1时,当前位置的数还要判断,所以i–,再i++等于i不变
*/
代码
class Solution {
public void sortColors(int[] nums) {
/**
思想: (本题不可以用排序,假如拍 102这种形式的就无法搞定了)
设置两个指针一个指向头p0,一个指向尾部p2, 根据题目要求,排成012的格式
然后开始遍历数组,当遍历到0时,与p0交换,并且p0指针后移
遍历到2时,与p2指针交换
但是要判断换到当前i位置的数是不是1,不为1时,当前位置的数还要判断,所以i--,再i++等于i不变
*/
// p0,p2分别指向头尾部
int p0,p2;
p0 = 0;
p2 = nums.length-1;
for(int i=0; i<=p2; i++) { // 注意这里i遍历到p2就行了
if(nums[i] == 0) {
// 当i位置为0时与p0位置交换,p0指针后移
int t = nums[i];
nums[i] = nums[p0];
nums[p0] = t;
p0++;
}
if(nums[i] == 2) {
// 当i位置为2时,与p2位置交换, p2指针前移动
int t = nums[i];
nums[i] = nums[p2];
nums[p2] = t;
p2--;
// 假如和p2位置换过来的是 0或者2的话,当前位置还得判断
if(nums[i] != 1) {
i--;
}
}
}
}
}
56. 合并区间
问题
思想
/**
思想: 按一维数组的第一个元素排升序
遍历二维数组(以一维为元素), 前一个一维度数组的第二个元素和后一个一维数组的第一个元素比较
若前一个大,则max更新为(前后两个一维数组的第二个元素中较大的数)
若前一个小,将[min,max]作为一个新的合并后的一维数组添加
最后再添加最后的区间
*/
代码
class Solution {
public int[][] merge(int[][] intervals) {
/**
思想: 按一维数组的第一个元素排升序
遍历二维数组(以一维为元素), 前一个一维度数组的第二个元素和后一个一维数组的第一个元素比较
若前一个大,则max更新为(前后两个一维数组的第二个元素中较大的数)
若前一个小,将[min,max]作为一个新的合并后的一维数组添加
最后再添加最后的区间
*/
// 如果intervals为空,直接返回
if(intervals == null) {
return intervals;
}
int start, len;
// 将数组按第一个元素排升序
Arrays.sort(intervals, (a,b)-> a[0]-b[0]);
// 创建一个用于返回的数组
List<int[]> res = new ArrayList<>();
len = intervals.length; // 获取长度
// 默认第一个区间为第一个数组的元素
start = intervals[0][0];
for(int i=1; i<len; i++) {// 注意这里在i=1开始遍历
if(intervals[i-1][1]>=intervals[i][0]) {
intervals[i][1] = Math.max(intervals[i-1][1], intervals[i][1]);
}else {
// max小于当前一维数组的第一个元素时
res.add(new int[] {start, intervals[i-1][1]});
// 更新起始
start = intervals[i][0];
}
}
// 将最后的一个区间添加
res.add(new int[]{start, intervals[len-1][1]});
return res.toArray(new int[0][]);
}
}
706. 设计哈希映射
代码
class MyHashMap {
/**
创建一个键值对的类
*/
private class Pair {
private int key;
private int value;
/* 无参构造 */
public Pair(){}
/* 带参构造 */
public Pair(int key, int value) {
this.key = key;
this.value = value;
}
/* 属性访问器 */
public int getKey() {
return key;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
}
private static final int BASE = 769;
private LinkedList[] data;
public MyHashMap() {
// 初始化链表
data = new LinkedList[BASE];
for (int i=0; i<BASE; ++i) {
data[i] = new LinkedList<Pair>();
}
}
public void put(int key, int value) {
int h = hash(key);
Iterator<Pair> iterator = iterator = data[h].iterator();
while(iterator.hasNext()) {
Pair pair = iterator.next();
if(pair.getKey() == key) {
pair.setValue(value);
return;
}
}
data[h].offerLast(new Pair(key, value));
}
public int get(int key) {
int h = hash(key);
Iterator<Pair> iterator = data[h].iterator();
while(iterator.hasNext()) {
Pair pair = iterator.next();
if(pair.getKey() == key) {
return pair.value;
}
}
return -1;
}
public void remove(int key) {
int h = hash(key);
Iterator<Pair> iterator = data[h].iterator();
while(iterator.hasNext()) {
Pair pair = iterator.next();
if(pair.getKey() == key) {
data[h].remove(pair);
return;
}
}
}
private static int hash(int key) {
return key % BASE;
}
}
/**
* Your MyHashMap object will be instantiated and called as such:
* MyHashMap obj = new MyHashMap();
* obj.put(key,value);
* int param_2 = obj.get(key);
* obj.remove(key);
*/
119. 杨辉三角 II
问题
思路
本题主要是生成杨辉三角, 根据如下规律
列标为0或者最后一列时赋值1
列表其他情况时等于上一行的j列与j-1列的加和
代码
class Solution {
public List<Integer> getRow(int rowIndex) {
List<List<Integer>> list = new ArrayList<>();
// 生成对应的空间,并填充
for(int i=0; i<=rowIndex; i++) {
List<Integer> listT = new ArrayList<>();
for(int j=0; j<=i; j++) {
if(j==0 || j==i) {
// 当j=0 或者j=i时填充1
listT.add(1);
}else {
listT.add( list.get(i-1).get(j) + list.get(i-1).get(j-1) );
}
}
// 将listT加入到list
list.add(listT);
}
return list.get(rowIndex);
}
}
48. 旋转图像
题目
思想
/**
思想: 使用辅助数组时,可以遍历整个矩阵,放到对应的旋转后的位置存到辅助数组
但如果使用原地算法,可以发现旋转时,其实将矩阵分成四个部分,
旋转后的矩阵是这四个位置顺时针转了一次
用一个变量暂存初始位置元素,然后旋转结束后将最开始的位置值填充
*/
代码
class Solution {
public void rotate(int[][] matrix) {
/**
思想: 使用辅助数组时,可以遍历整个矩阵,放到对应的旋转后的位置存到辅助数组
但如果使用原地算法,可以发现旋转时,其实将矩阵分成四个部分,
旋转后的矩阵是这四个位置顺时针转了一次
用一个变量暂存初始位置元素,然后旋转结束后将最开始的位置值填充
*/
int n = matrix.length;
for(int i=0; i<n/2; i++) {
for(int j=0; j<(n+1)/2; j++) {
int t = matrix[i][j];
matrix[i][j] = matrix[n-1-j][i];
matrix[n-1-j][i] = matrix[n-1-i][n-1-j];
matrix[n-1-i][n-1-j] = matrix[j][n-1-i];
matrix[j][n-1-i] = t;
}
}
}
}
59. 螺旋矩阵 II
问题
思想
/**
思想:
输入n,将有n*n的元素
将填充的元素设置为四个方向, 通过一个变量控制方向填充
*/
代码
class Solution {
public int[][] generateMatrix(int n) {
/**
思想:
输入n,将有n*n的元素
将填充的元素设置为四个方向, 通过一个变量控制方向填充
*/
// 创建一个数组
int arr[][] = new int[n][n];
int p=0, k=1;// p用于控制方向,k用于叠加
int i=0,j=0;
while(k<=n*n) {
if(p==0) {
// 向右方向
while(j<n && arr[i][j]==0) {
arr[i][j++] = k++;
}
// 调整
i++;
j--;
p=1;
}
if(p==1) {
// 向下方向
while(i<n && arr[i][j]==0) {
arr[i++][j] = k++;
}
// 调整
i--;
j--;
p=2;
}
if(p==2) {
// 向左方向
while(j>=0 && arr[i][j]==0) {
arr[i][j--] = k++;
}
// 调整
i--;
j++;
p=3;
}
if(p==3) {
// 向上方向
while(i>=0 && arr[i][j]==0) {
arr[i--][j] = k++;
}
// 调整
i++;
j++;
p=0;
}
}
return arr;
}
}
240. 搜索二维矩阵 II
问题
思想
/**
思想: 因为该矩阵有一个规律就是横向升序,竖向升序
我们以右上角为起点,向左就是降序,向下就是升序,可以当成二分查找树来想
当目标值大于当前值时向下找,当目标值小于当前值时向左找
找到则返回true
整个遍历结束找不到则无目标值返回false
*/
代码
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
/**
思想: 因为该矩阵有一个规律就是横向升序,竖向升序
我们以右上角为起点,向左就是降序,向下就是升序,可以当成二分查找树来想
当目标值大于当前值时向下找,当目标值小于当前值时向左找
找到则返回true
整个遍历结束找不到则无目标值返回false
*/
// 判断是否为空的
if(matrix.length == 0 || matrix[0].length == 0) {
return false;
}
// 设置两个指针,一个作为行指针指向开始行,一个作为列指针指向最后一列
int row = 0, col = matrix[0].length-1;
// 寻找目标值
while(row<matrix.length && col>=0) {
if(target>matrix[row][col]) {
// 目标值大,向下找
row++;
}else if(target<matrix[row][col]) {
// 目标值小,向左找
col--;
}else {
// 目标值等于
return true;
}
}
// 循环结束,未找到
return false;
}
}
334. 递增的三元子序列
问题
思想
/**
思路: 本题要求找到一个升序的三元组
先设置一个first为数组第一个元素, 设置second为最大的数
然后从下标为1遍历数组
当有 数值 在first与second直接时,更新为second的值
当有 数值 小于first时,更新为first的值
当有数值大于 second的值时,说明存在升序的三元组
*/
代码
class Solution {
public boolean increasingTriplet(int[] nums) {
/**
思路: 本题要求找到一个升序的三元组
先设置一个first为数组第一个元素, 设置second为最大的数
然后从下标为1遍历数组
当有 数值 在first与second直接时,更新为second的值
当有 数值 小于first时,更新为first的值
当有数值大于 second的值时,说明存在升序的三元组
*/
// 数组的长度
int len = nums.length;
// 数组长度小于3时,直接返回false
if(len<3) return false;
// 初始化first与second
int first = nums[0]; // 初始first为数组第一个元素
int second = Integer.MAX_VALUE; // 初始second为最大值
// 开始遍历数组(从第二个元素开始)
for(int i=1; i<len; i++) {
int num = nums[i];
if(num>second) {
// 当有值大于第二大的值,说明有三元组为升序
return true;
}else if(num>first) {
// 当有值处于最小与第二小的值之间时,更新第二小为更小的数
second = num;
}else {
// 当num小于最小的值时,更新最小值
first = num;
}
}
// 整个遍历结束后仍未找到则返回false
return false;
}
}
238. 除自身以外数组的乘积
问题
思路
/**
算法思想: 保存每个除了本元素外的其他元素的乘积的最简单的思想
1. 将所有元素相乘
2. 然后遍历, 用总成绩除以遍历到的乘积
显然本题禁用了除法,所以可以用两次循环实现,除本身外的元素乘积
1. 第一轮循环正序遍历,用于记录,到该位置前的元素的乘积
2. 第二轮循环倒序遍历,用于补充从后向前乘到该元素的乘积
*/
代码
class Solution {
public int[] productExceptSelf(int[] nums) {
/**
算法思想: 保存每个除了本元素外的其他元素的乘积的最简单的思想
1. 将所有元素相乘
2. 然后遍历, 用总成绩除以遍历到的乘积
显然本题禁用了除法,所以可以用两次循环实现,除本身外的元素乘积
1. 第一轮循环正序遍历,用于记录,到该位置前的元素的乘积
2. 第二轮循环倒序遍历,用于补充从后向前乘到该元素的乘积
*/
// 创建保存最后结果的数组
int res[] = new int[nums.length];
// p用于正序遍历时记录到i位置前的元素乘积, q用于倒序白能力记录到i元素位置后的元素乘积
int p=1,q=1;
// 第一轮循环
for(int i=0; i<nums.length; i++) {
res[i] = p;
p*=nums[i]; // 用于保存到i位置前的元素乘积
}
// 第二轮循环
for(int i = nums.length - 1; i>0; i--) {
q *= nums[i]; // q用于保存从后向前到i位置后的元素的乘积
res[i-1] *= q;
}
return res;
}
}
435. 无重叠区间
问题
思想
/**
思想: 本题用贪心来做
题目让移除最少的区间数量,从而使得区间互不重叠
换个思路,就是找到最大无重叠区间的数量
因此,我们可以按右区间进行排序,从而第一个区间才可能是最小的右区间
然后设初值right为第一个区间的右区间,循环遍历
当right小于后续的区间的左区间时,可合并为一个与其他区间互不重叠的区间,
并以该区间为新的初始的区间,该区间的右区间为新的right
贪心的重复如此重复
遍历结束后边统计好最大不重叠区间了
最后题目让求去除最小的区间个数,也就是总的区间数减去最大区间个数
*/
代码
class Solution {
public int eraseOverlapIntervals(int[][] intervals) {
/**
思想: 本题用贪心来做
题目让移除最少的区间数量,从而使得区间互不重叠
换个思路,就是找到最大无重叠区间的数量
因此,我们可以按右区间进行排序,从而第一个区间才可能是最小的右区间
然后设初值right为第一个区间的右区间,循环遍历
当right小于后续的区间的左区间时,可合并为一个与其他区间互不重叠的区间,
并以该区间为新的初始的区间,该区间的右区间为新的right
贪心的重复如此重复
遍历结束后边统计好最大不重叠区间了
最后题目让求去除最小的区间个数,也就是总的区间数减去最大区间个数
*/
// 数组空时直接返回0
if(intervals.length == 0) {
return 0;
}
// 将数组按照右区间进行排序
Arrays.sort(intervals, (a,b)-> a[1]-b[1]);
// 设right用于记录右区间,ans用于记录最大不重叠区间个数,注意默认为1
int right=intervals[0][1], ans=1;
// 从第2个区间(下标为1)开始遍历
for(int i=1; i<intervals.length; i++) {
if(right<=intervals[i][0]) { // 注意,当right等于某区间左端时,也算一个独立区间
// right小于当前遍历的区间左端时,说明前面已成一个不重叠区间
ans++; // 不重叠区间+1
right = intervals[i][1]; // 更新right
}
}
// 返回最少减去的区间
return intervals.length - ans;
}
}
560. 和为 K 的子数组
问题
思想(暴力破解法)
/**
方案1: 暴力破解法
两次循环, 第一次循环确定起始点,第二次循环累加到尾部
在这两次循环中,根据累次加和判断是否等于目标值进行统计
当加和大于目标值时,退出 (注意,如果给定数据为有序时,本行生效)
*/
代码(暴力破解法)
class Solution {
public int subarraySum(int[] nums, int k) {
/**
方案1: 暴力破解法
两次循环, 第一次循环确定起始点,第二次循环累加到尾部
在这两次循环中,根据累次加和判断是否等于目标值进行统计
当加和大于目标值时,退出 (注意,如果给定数据为有序时,本行生效)
*/
int sum=0, count=0; // 用于统计加和和统计符合的个数
for(int i=0; i<nums.length; i++) {
sum = 0; // 起点依次后移
for(int j=i; j<nums.length; j++) {
// 以i为起点累计加和
sum+=nums[j];
if(sum==k) {
// 等于目标值时,计数加1
count++;
}
}
}
return count;
}
}
思想(前缀加和法)
/**
方案2 先求出前缀和法
思想: 先用一个数组记录前缀和(注意, 元素为n个记录前缀和的数组为n加1)
记录前缀和: 第一个元素的前缀无为0; 第二个元素的前缀为第一个元素向前的和
第三个元素的前缀和为第二个元素到头的和, 第n个元素前缀和为第n-1个元素向前到头的和
因此以n向前统计作为前缀和, 以n+1的位置存储
当有记录前缀和的数组后
外层循环:以遍历的结果为起始点,
内层循环:然后从该元素后的前缀和数组,减去该元素的前缀,看是否等于目标值进行统计
*/
代码(前缀和版本)
class Solution {
public int subarraySum(int[] nums, int k) {
/**
方案2 先求出前缀和法
思想: 先用一个数组记录前缀和(注意, 元素为n个记录前缀和的数组为n加1)
记录前缀和: 第一个元素的前缀无为0; 第二个元素的前缀为第一个元素向前的和
第三个元素的前缀和为第二个元素到头的和, 第n个元素前缀和为第n-1个元素向前到头的和
因此以n向前统计作为前缀和, 以n+1的位置存储
当有记录前缀和的数组后
外层循环:以遍历的结果为起始点,
内层循环:然后从该元素后的前缀和数组,减去该元素的前缀,看是否等于目标值进行统计
*/
int len = nums.length; // 记录数组的长度
// 初始化记录前缀和的数组(n+1个元素)
int[] preSum = new int[len+1];
preSum[0] = 0; // 第一个元素无前缀
for(int i=0; i<len; i++) {
preSum[i+1] = preSum[i] + nums[i];
}
int count=0; // 用于记录等于目标值的连续数组
for(int i=0; i<len; i++) {// 以第i个位置为起始的连续数组
for(int j=i; j<len; j++) {
// 第i个位置后的前缀和减去第i个位置前缀等于目标值的
if(preSum[j+1] - preSum[i] == k) {
// 计数加1
count++;
}
}
}
return count;
}
}
字符串
415. 字符串相加(⭐ 大数相加)
问题
思想
/**
大数加法的实现: 通过模拟加法竖式相加运算,具体思路如下
两个字符串对应位置字符相加, 因为是十进制,所以该位置留下模除十的数,整除十得到的数进位与更高位的位置加和相加
同理直到两个字符串都遍历尽
假如有字符串先遍历尽,则用0作为加和
*/
代码
class Solution {
public String addStrings(String num1, String num2) {
/**
大数加法的实现: 通过模拟加法竖式相加运算,具体思路如下
两个字符串对应位置字符相加, 因为是十进制,所以该位置留下模除十的数,整除十得到的数进位与更高位的位置加和相加
同理直到两个字符串都遍历尽
假如有字符串先遍历尽,则用0作为加和
*/
// 拿到两个字符串尾部指针,以及一个用于保存进位的变量
int i=num1.length() - 1, j = num2.length() - 1, jw = 0;
// 保存加和的一个可变字符串
StringBuilder res = new StringBuilder("");
while(i>=0 || j>=0) {// 每个字符串都要遍历结束,从尾到头
// 取出两个对应位置的数相加
int n1 = i >= 0 ? num1.charAt(i) - '0' : 0;
int n2 = j >= 0 ? num2.charAt(j) - '0' : 0;
// 暂存两个对应位置的加和 以及 进位的值相加
int t = n1 + n2 + jw;
// 看本次是否有进位
jw = t/10;
// 看本位的数为多少
res.append(t % 10);
// 两字符串指针向前移动
i--;
j--;
}
// 判断最后是否还有进位
if(jw>0) { // 严格意义上等于1就可以判断,因为每个位都是十进制数相加,进位最多为1
res.append(jw);
}
// 注意都是添加到字符串后面的,要反转一下
return res.reverse().toString();
}
}
409. 最长回文串
问题
思想
/**
思想: 字符的ascll码可以用0-127表示,创建一个128位的一维数组记录元素出现的次数
遍历字符串,将出现的字符统计到临时数组中,没逢一个字符出现两次,就统计到构造的回文串中,并且清空该字符出现次数
最后判断回文串长度是否小于原字符串,如若小于说明存在奇数个的字符,作为回文串的中心
*/
代码
class Solution {
public int longestPalindrome(String s) {
/**
思想: 字符的ascll码可以用0-127表示,创建一个128位的一维数组记录元素出现的次数
遍历字符串,将出现的字符统计到临时数组中,没逢一个字符出现两次,就统计到构造的回文串中,并且清空该字符出现次数
最后判断回文串长度是否小于原字符串,如若小于说明存在奇数个的字符,作为回文串的中心
*/
// 处理出现次数
int[] occurs = new int[128];
// 统计回文串字数
int ans=0;
for(int i=0; i<s.length(); i++) {
char c = s.charAt(i);
occurs[c]++;
if(occurs[c] == 2) { // 每逢出现两次,可加入到回文串长度中
ans+=2;
// 将该位置清空
occurs[c]=0;
}
}
// 判断是否有奇数次字符
if(ans < s.length()) ans++;
return ans;
}
}
290. 单词规律
问题
思想
/**
代码思想: 先将单词组成的字符传,拆分成数组
首先字符组成串长度和数组长度相等
循环遍历字符串,将对应位置的字符和单词数组加入到哈希表
假如哈希表对应元素一致,则继续
不一致,则说明不同规则返回false
*/
代码
class Solution {
public boolean wordPattern(String pattern, String s) {
/**
代码思想: 先将单词组成的字符传,拆分成数组
首先字符组成串长度和数组长度相等
循环遍历字符串,将对应位置的字符和单词数组加入到哈希表
假如哈希表对应元素一致,则继续
不一致,则说明不同规则返回false
*/
String[] words = s.split(" "); // 以空格为分割,分割为数组
// 判断数组长度和串长是否相等
if(words.length != pattern.length()) {
return false;
}
// 创建辅助的哈希表
Map<Object, Integer> map = new HashMap<>();
// 遍历判断
for(Integer i=0; i<words.length; i++) {
if(map.put(pattern.charAt(i), i) != map.put(words[i], i)) {
return false;
}
}
return true;
}
}
763. 划分字母区间
问题
思想
/**
实现思想:
1. 先用一个辅助的数组记录每个字符出现的最终位置
2. 再次遍历时
设置end为每个片段的结束位置,当在访问到end前如果访问到结束位置大于当前end的则更新end为更大值
当i遍历到end位置时,说明一个单独的片段可被分出,并设置该结束点+1位置为新的起始点
而每个片段的元素数为 end - start + 1
*/
代码
class Solution {
public List<Integer> partitionLabels(String s) {
/**
实现思想:
1. 先用一个辅助的数组记录每个字符出现的最终位置
2. 再次遍历时
设置end为每个片段的结束位置,当在访问到end前如果访问到结束位置大于当前end的则更新end为更大值
当i遍历到end位置时,说明一个单独的片段可被分出,并设置该结束点+1位置为新的起始点
而每个片段的元素数为 end - start + 1
*/
// 创建可以保存字符最后出现位置的辅助数组
int lastArr[] = new int[26];
// 遍历字符串,获取到每个字符最后出现位置
for(int i=0; i<s.length(); i++) {
lastArr[s.charAt(i) - 'a'] = i; // 注意这里之所以保存每个字符最后出现位置,是这里可以覆盖
}
// 用以动态记录每个片段的start与end
int start=0, end=0;
// 用于保存每个片段的list
List<Integer> list = new ArrayList<>();
// 再次遍历,确定每个片段包含的元素个数
for(int i=0; i<s.length(); i++) {
// end更新规律: 未遍历到end位置时,访问的字符有大于end的,则更新end,以确保在一个片段出现的字符不会出现在其他片段
end = Math.max(end, lastArr[s.charAt(i)-'a']);
if(i==end) {
// i等于end,说明划分出一个片段
list.add(end-start+1); // end-start+1表示该片段包含的元素个数
start = end + 1; // start表示每个片段的起始位置
}
}
return list;
}
}
49. 字母异位词分组
题目
思想
/**
思想: 因为异位词中单词包含的字母一致
所以可以通过将遍历到的单词的字母进行排序,得到相同的键位,存到哈希表中
*/
代码
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
/**
思想: 因为异位词中单词包含的字母一致
所以可以通过将遍历到的单词的字母进行排序,得到相同的键位,存到哈希表中
*/
Map<String, List<String>> map = new HashMap<>(); // 用于按键名保存异位词
for(String str : strs) { // 遍历所有的单词
// 将单词转换成数组排序得到对应的键位名
char[] array = str.toCharArray();
Arrays.sort(array);
// 得到对应的键位
String key = new String(array);
// 获取到当前map中该键位对应的列表,如何没有该键位则生成一个保存空的列表
List<String> list = map.getOrDefault(key, new ArrayList<String>());
// 将该单词加入该列表
list.add(str);
// 重新将该列表存储到map
map.put(key, list);
}
return new ArrayList<List<String>>(map.values());
}
}
43. 字符串相乘(大数)
问题
思想
/**
大数乘法思想:
将两个数用竖式乘法的形式计算。
将两个数的乘法,看成其中一个数与另一个数每一位的乘积的加和
注意乘以一个数的每一位时,要注意处理十位、百位、千位的乘积倍数
再将一个数乘以另一个数的每个位的数进行一个大数加法运算即可
*/
代码
class Solution {
public String multiply(String num1, String num2) {
/**
大数乘法思想:
将两个数用竖式乘法的形式计算。
将两个数的乘法,看成其中一个数与另一个数每一位的乘积的加和
注意乘以一个数的每一位时,要注意处理十位、百位、千位的乘积倍数
再将一个数乘以另一个数的每个位的数进行一个大数加法运算即可
*/
// 当有一个字符串为0时,则乘积为0
if(num1.equals("0") || num2.equals("0")) {
return "0";
}
// 用于保存最后的结果(每一位乘积的加和)
String res = "0";
// num2 逐位与 num1 相乘
for(int i = num2.length() - 1; i>=0; i--) {
int carry = 0; // 当乘积超过9时进位
StringBuilder temp = new StringBuilder();
// 高位补0
for(int j=0; j<num2.length()-1-i; j++) {
temp.append(0);
}
int n2 = num2.charAt(i) - '0'; // 参与运算的nums2第i位的数
// numsw的第i位数字 与 nums1 相乘过程
for(int j=num1.length() - 1; j>=0 || carry != 0; j--) {
int n1 = j<0?0:num1.charAt(j)-'0'; // 竖式相乘
int product = (n1 * n2 + carry) % 10; // 该位置留下的数
temp.append(product); // 添加到临时记录的串中
carry = (n1 * n2 +carry) / 10; // 这一次的进位记录
}
// 将该位运算的乘积相加到最终结果(大数加法)
res = bigNumAdd(res, temp.reverse().toString());
}
// 返回最终结果
return res;
}
/**
大数加法
*/
public String bigNumAdd(String num1, String num2) {
StringBuilder resStr = new StringBuilder(); // 用于保存大数计算的最终结果
int carry = 0; // 用于保存进位
for(int i=num1.length()-1, j=num2.length()-1; i>=0||j>=0||carry!=0; i--,j--) { // 大数竖式运算逻辑
int n1 = i<0?0:num1.charAt(i)-'0';
int n2 = j<0?0:num2.charAt(j)-'0';
int sum = (n1+n2+carry) % 10;
resStr.append(sum);
carry = (n1+n2+carry) / 10; // 更新进位
}
return resStr.reverse().toString();
}
}
187. 重复的DNA序列
问题
思想
/**
思想: 用哈希表实现, 截取长度为10的字串为键名统计出现次数
*/
代码
class Solution {
static final int L = 10; // 子串的长度
public List<String> findRepeatedDnaSequences(String s) {
/**
思想: 用哈希表实现, 截取长度为10的字串为键名统计出现次数
*/
List<String> ans = new ArrayList<>();
Map<String, Integer> cnt = new HashMap<>();
for(int i=0; i<= s.length() - L; i++) {
String sub = s.substring(i, i+L); // 截取子串
cnt.put(sub, cnt.getOrDefault(sub, 0) + 1); // 该字串没出现
if(cnt.get(sub)==2) { // 这里非常巧妙,因为出现次数超过1次的只统计1次就行
ans.add(sub);
}
}
return ans;
}
}
5. 最长回文子串
问题
动态规划思想
/**
动态规划的思想: 用一个 boolean dp[l][r] 表示字符串从 i 到 j 这段是否为回文。试想如果 dp[l][r]=true,
我们要判断 dp[l-1][r+1] 是否为回文。只需要判断字符串在(l-1)和(r+1)两个位置是否为相同的字符,
是不是减少了很多重复计算。 进入正题,动态规划关键是找到初始状态和状态转移方程。 初始状态,l=r 时,
此时 dp[l][r]=true。 状态转移方程,dp[l][r]=true 并且(l-1)和(r+1)两个位置为相同的字符,此时 dp[l-1][r+1]=true。
*/
动态规划代码
class Solution {
public String longestPalindrome(String s) {
/**
动态规划的思想: 用一个 boolean dp[l][r] 表示字符串从 i 到 j 这段是否为回文。试想如果 dp[l][r]=true,
我们要判断 dp[l-1][r+1] 是否为回文。只需要判断字符串在(l-1)和(r+1)两个位置是否为相同的字符,
是不是减少了很多重复计算。 进入正题,动态规划关键是找到初始状态和状态转移方程。 初始状态,l=r 时,
此时 dp[l][r]=true。 状态转移方程,dp[l][r]=true 并且(l-1)和(r+1)两个位置为相同的字符,此时 dp[l-1][r+1]=true。
*/
if(s == null || s.length() < 2) {
return s; // 字符串长度为1时就是回文串了
}
int strLen = s.length(); // 获取字符串的长度
int maxStart = 0; // 最长回文串的起点
int maxEnd = 0; // 最长回文串的终点
int maxLen = 1; // 最长回文串的长度(最短一定为1)
boolean[][] dp = new boolean[strLen][strLen]; //动态规划的数组
for(int r=1; r<strLen; r++) { // 以r位置为中心判断最长的回文串
for(int l=0; l<r; l++) {
if(s.charAt(l) == s.charAt(r) && (r-l<=2 || dp[l+1][r-1])) {
// 两个字符串相等, 并且要么两个字符相连接,要么以两个字符为始终的串为回文串
dp[l][r] = true;
if(r - l + 1 > maxLen) {
// 当有更长的回文串时,更新
maxLen = r - l + 1;
maxStart = l;
maxEnd = r;
}
}
}
}
return s.substring(maxStart, maxEnd+1);
}
}
链表
2. 两数相加(大数加法的链表结构)
问题
思想
/**
算法思想:大数加法(链表存储结构)
由于链表是倒序链接的,所以正好符合大数加法,从低位对应相加
*/
代码
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
/**
算法思想:大数加法(链表存储结构)
由于链表是倒序链接的,所以正好符合大数加法,从低位对应相加
*/
ListNode pre = new ListNode(0);
ListNode cur = pre;
int carry = 0; // 进位记录
while(l1 != null || l2 != null) {
// 两个链表全部遍历结束后,加法结束
int x = l1==null?0:l1.val;
int y = l2==null?0:l2.val;
int sum = (x+y+carry)%10; // 当前位置留着
carry = (x+y+carry) / 10; // 更新进位
cur.next = new ListNode(sum);
cur = cur.next;
if(l1 != null) {
l1 = l1.next;
}
if(l2 != null) {
l2 = l2.next;
}
}
if(carry == 1) {
cur.next = new ListNode(carry);
}
return pre.next;
}
}
142. 环形链表 II
问题
思想
/**
思想: 判断是否有环,如果有环则返回入环口
通过快慢指针实现,快指针一次走两步,慢指针一次走一步
如果快指针结束,则无环,否则快指针和慢指针一定会相遇
*/
代码
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode detectCycle(ListNode head) {
/**
思想: 判断是否有环,如果有环则返回入环口
通过快慢指针实现,快指针一次走两步,慢指针一次走一步
如果快指针结束,则无环,否则快指针和慢指针一定会相遇
*/
ListNode fast = head, slow = head; // 在开头设置两个指针,一个快一个慢
while(true) {
if(fast == null || fast.next == null) return null; // 如果快指针到尾部,则无环
fast = fast.next.next;
slow = slow.next;
if(fast == slow) break; // 当快慢指针相遇时终止循环
}
// 将快指针指向头, 慢指针在相遇处
fast = head;
while(slow != fast) {
// 当快慢指针再次相遇时就是入口位置了
slow = slow.next;
fast = fast.next;
}
return fast;
}
}
160. 相交链表
问题
思想
/**
算法思想:
先让更长一些的链表跑,跑到两个链表等长时,再同时跑
两结点相遇时返回相遇结点,遍历结束还不相遇时,返回null
*/
代码
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
/**
算法思想:
先让更长一些的链表跑,跑到两个链表等长时,再同时跑
两结点相遇时返回相遇结点,遍历结束还不相遇时,返回null
*/
int Alen, Blen;
Alen = getLinkLen(headA);
Blen = getLinkLen(headB);
if(Alen > Blen) {
// A链长,A先走
for(int i=0; i<Alen-Blen; i++) {
headA = headA.next;
}
}else {
// B链长,B先走
for(int i=0; i<Blen-Alen; i++) {
headB = headB.next;
}
}
// 同步走
while(headA!=null) {
if(headA == headB) {
// 找到两链表交汇点
return headA;
}
headA = headA.next;
headB = headB.next;
}
// 最后无交集返回null
return null;
}
// 获取链表长度
public int getLinkLen(ListNode T){
int len=0;
while(T!=null) {
len++;
T = T.next;
}
return len;
}
}
思想二(巧妙解法)
代码(巧解)
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
/**
思想二: 每个链表逐个一步一步后移
*/
if(headA == null || headB == null) return null;
ListNode pA = headA, pB = headB;
while(pA != pB) {
pA = pA == null ? headB:pA.next; // 当一个链表到头时,
pB = pB == null ? headA:pB.next;
}
return pA;
}
82. 删除排序链表中的重复元素 II
若清除重复元素,只留一个时
class Solution {
public ListNode deleteDuplicates(ListNode head) {
/**
思想: 如果下一个结点和当前结点相同,则当前结点指向下一个结点的下一个结点
如果下一个结点不和当前结点相同,则当前结点后移
*/
// 如果head为空,返回
if(head == null) return head;
ListNode T = head;
while(T.next!=null) {
if(T.next.val == T.val) {// 当前与下一个相等
T.next = T.next.next;
}else {
T = T.next;
}
}
return head;
}
}
问题
思想
/**
思想: 注意本题要删除所有的带有重复的结点,重复的结点不会留下一个
因为假如全部为重复结点时,链表要全部删除,所以我们设置一个哑结点,充当头结点
当遍历下一个位置与下下一个位置元素相同时,记录该元素,循环将等于该元素的所有结点删除
最后返回头结点的next
*/
代码
class Solution {
public ListNode deleteDuplicates(ListNode head) {
/**
思想: 注意本题要删除所有的带有重复的结点,重复的结点不会留下一个
因为假如全部为重复结点时,链表要全部删除,所以我们设置一个哑结点,充当头结点
当遍历下一个位置与下下一个位置元素相同时,记录该元素,循环将等于该元素的所有结点删除
最后返回头结点的next
*/
// head空直接返回
if(head == null) return head;
// 创建哑结点
ListNode dummy = new ListNode(0, head); // dummy.next = head
ListNode T =dummy;
while(T.next!=null && T.next.next != null) {
if(T.next.val == T.next.next.val) {
// 当下一个元素等于下下一个元素时
// 记录该值
int x = T.next.val;
// 将等于x的结点全部清除
while(T.next!=null && T.next.val == x) {
T.next = T.next.next;
}
}else {
T = T.next;
}
}
return dummy.next;
}
}
24. 两两交换链表中的节点
问题
代码
class Solution {
public ListNode swapPairs(ListNode head) {
ListNode pre = new ListNode(0, head); // 初始一个指向head的头结点
ListNode temp = pre; // temp结点用于临时操作链表
while(temp.next !=null && temp.next.next !=null) {
ListNode start = temp.next; // 用start记录要交换的前一个结点
ListNode end = temp.next.next; // 用end 记录交换的后一个结点
temp.next = end; // 用临时结点指向第二个指针
start.next = end.next; // 前一个结点指向后一个结点所指向的位置
end.next = start; // 后一个结点指向前一个结点
temp = start;
}
return pre.next;
}
}
707. 设计链表
问题
代码
class MyLinkedList {
private int size;
ListNode head;
public MyLinkedList() {
// 无参构造器
size = 0;
head = new ListNode(0);
}
public int get(int index) { // 获取下标为index的元素
if(index < 0 || index >= size) {
// 越界
return -1;
}
ListNode cur = head;
for(int i=0; i<=index; i++) {
cur = cur.next;
}
return cur.val;
}
public void addAtHead(int val) { // 插入到头
// 直接调用按位置插入
this.addAtIndex(0, val);
}
public void addAtTail(int val) { // 插入到尾部
this.addAtIndex(size, val);
}
public void addAtIndex(int index, int val) { // 插入到指定位置
if(index>size || index<0 ) { // 下标不符合
return;
}
this.size++; // 数量加1
ListNode pred = head;
// 遍历到插入的位置
for(int i=0; i<index; i++) {
pred = pred.next;
}
ListNode toAdd = new ListNode(val);
toAdd.next = pred.next;
pred.next = toAdd;
}
public void deleteAtIndex(int index) { // 删除指定位置的结点
if(index<0 || index>=size) {
// 下标溢出
return;
}
this.size--;
ListNode pred = head;
// 找到index位置
for(int i=0; i<index; i++) {
pred = pred.next;
}
pred.next = pred.next.next;
}
}
143. 重排链表
问题
思想
/**
思想: 本题将链表右半端反转后和左半端交叉插入
1. 通过快慢指针找到中间结点,快指针走到尾部,慢指针就走到中间了
2. 将右侧链表原地反转(遍历链表,通过头插法,将链表反转)
3. 将左侧链表和反转后的链表交叉合并
*/
代码
class Solution {
public void reorderList(ListNode head) {
/**
思想: 本题将链表右半端反转后和左半端交叉插入
1. 通过快慢指针找到中间结点,快指针走到尾部,慢指针就走到中间了
2. 将右侧链表原地反转(遍历链表,通过头插法,将链表反转)
3. 将左侧链表和反转后的链表交叉合并
*/
if(head == null) {
return;
}
ListNode mid = middleNode(head); // 找到中间结点
ListNode L1 = head; // 左半段链表
ListNode L2 = mid.next; // 右半段链表
mid.next = null; // 断链
L2 = reverseList(L2); // 将右半端链表反转
mergeList(L1, L2); // 将左半端链表和右侧反转后的链表合并
}
/** 寻找中间结点 */
public ListNode middleNode(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while(fast.next != null && fast.next.next != null) {
slow = slow.next; // 慢指针一次走一格
fast = fast.next.next; // 快指针一次走两格
}
// 快指针走到尾部,慢指针走到中部了
return slow;
}
/** 反转链表 */
public ListNode reverseList(ListNode head) {
// 遍历链表,通过头插法,将链表反转
ListNode prev = null; // 链表头
ListNode curr = head; // 用于遍历取出结点
while(curr != null) {
ListNode nextTemp = curr.next; // 保存后续的链表
curr.next = prev; // 头插
prev = curr;
curr = nextTemp;
}
return prev;
}
/** 交叉合并 */
public void mergeList(ListNode L1, ListNode L2) {
ListNode L1_tmp;
ListNode L2_tmp;
while(L1!=null && L2!=null) {
L1_tmp = L1.next;
L2_tmp = L2.next;
L1.next = L2;
L1 = L1_tmp;
L2.next = L1;
L2 = L2_tmp;
}
}
}
25. K 个一组翻转链表
问题
思想
代码
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
ListNode dummy = new ListNode(0, head); // 创建链表头
ListNode pre = dummy; // 设置一个记录要反转的链表头
ListNode end = dummy; // 设置一个记录要反转的链表尾部
while(end.next != null) { //当end遍历到null说明不需要反转了,整条链到尾了
for(int i=0; i<k && end != null; i++) end = end.next; // 寻找长度为k的需要交换的链表尾部
if(end == null) break; // end到尾部说明整条链表到尾部了,不用再反转了
ListNode start = pre.next; //
ListNode next = end.next; // 防止断链,保留后续结点
end.next = null; // 断链,操作前面的链表反转
pre.next = reverse(start); // 将这k个结点进行反转
start.next = next; // 将链表再接到一起
// 重置pre,end的重新位置,寻找下一个包括k个结点的子链
pre = start;
end = pre;
}
return dummy.next;
}
/** 链表反转 */
public ListNode reverse(ListNode head) {
ListNode pre = null;
ListNode curr = head;
while(curr != null) {
ListNode next = curr.next; // 保存后续的结点
curr.next = pre; // 隔离出来的结点以头插法插入到pre
pre = curr;
curr = next;
}
return pre;
}
}
栈
155. 最小栈
问题
思想
弄一个保存最小数的栈,当元素入栈时,判断是否更小,也存储在最小栈的栈顶
代码
class MinStack {
Deque<Integer> xStack;
Deque<Integer> minStack; // 用于保存最小值的栈
public MinStack() {
xStack = new LinkedList<Integer>();
minStack = new LinkedList<Integer>();
minStack.push(Integer.MAX_VALUE); // 默认设置最大数为最小值
}
public void push(int val) { // 加入一个新的元素
xStack.push(val);
minStack.push(Math.min(minStack.peek(), val)); // minStack保存的是加入当前元素后,整个栈的最小值
}
public void pop() { // 弹出元素
xStack.pop();
minStack.pop();
}
public int top() { // 返回栈头元素
return xStack.peek();
}
public int getMin() { // 返回最小元素
return minStack.peek();
}
}
1249. 移除无效的括号
问题
思想
/**
思想:
遇到左括号入栈左括号的下标
遇到右括号,判断有左括号则弹出一个最近的左括号,没有左括号则记录该右括入要删除的下标
最后遍历栈是否为空,不空则,将栈中左括号的位置加入到要删除的set中
然后根据要删除的结点
*/
代码
class Solution {
public String minRemoveToMakeValid(String s) {
/**
思想:
遇到左括号入栈左括号的下标
遇到右括号,判断有左括号则弹出一个最近的左括号,没有左括号则记录该右括入要删除的下标
最后遍历栈是否为空,不空则,将栈中左括号的位置加入到要删除的set中
然后根据要删除的结点
*/
Set<Integer> indexesToRemove = new HashSet<>();
Stack<Integer> stack = new Stack<>();
for(int i=0; i<s.length(); i++) { // 遍历字符串
if(s.charAt(i) == '(') {
// 当遇到左括号,加入栈
stack.push(i);
}
if(s.charAt(i) == ')') {
// 当遇到右括号时
if(stack.isEmpty()) {
// 如果此时栈空,说明此时的右括号为无效括号,记录移除
indexesToRemove.add(i);
}else {
// 如果此时栈中有左括,则弹出一个,相当于与此右括号匹配
stack.pop();
}
}
}
// 判断栈中是否有多余的左括号
while(!stack.isEmpty()) indexesToRemove.add(stack.pop());
StringBuilder sb = new StringBuilder();
for(int i=0; i<s.length(); i++) { // 将要移除位置移除
if(!indexesToRemove.contains(i)) { // 要删除的就不加入
sb.append(s.charAt(i));
}
}
return sb.toString();
}
}
1823. 找出游戏的获胜者
问题
代码
class Solution {
public int findTheWinner(int n, int k) {
Queue<Integer> queue = new ArrayDeque<Integer>(); // 创建一个保存数据的队列
for(int i=1; i<=n; i++) {
// 将1-n的元素入队
queue.offer(i);
}
while(queue.size()>1) {
for(int i=1; i<k; i++) {// 输出第k个位置的数
queue.offer(queue.poll()); // 未到输出的加入到队尾
}
// 第k个元素 出队
queue.poll();
}
return queue.peek();
}
}
树
108. 将有序数组转换为二叉搜索树
问题
思想
/**
思想: 二叉搜索树的中心遍历为升序
可以将每一段的中心当作根结点
左边为左子树,右边为右子树
依次递归
(可以想象为画成二叉折半树的样子)
*/
代码
class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
/**
思想: 二叉搜索树的中心遍历为升序
可以将每一段的中心当作根结点
左边为左子树,右边为右子树
依次递归
(可以想象为画成二叉折半树的样子)
*/
return helper(nums, 0, nums.length-1);
}
public TreeNode helper(int[] nums, int left, int right) {
if(left > right) {
return null;
}
// 总是选择中间位置的左侧作为根结点
int mid = (left+right)/2;
TreeNode root = new TreeNode(nums[mid]);
root.left = helper(nums, left, mid-1);
root.right = helper(nums, mid+1, right);
return root;
}
}
105. 从前序与中序遍历序列构造二叉树
问题
代码
class Solution {
private Map<Integer, Integer> indexMap;
public TreeNode buildTree(int[] preorder, int[] inorder) {
int n = preorder.length;
// 构造哈希映射,便于快速定位根节点
indexMap = new HashMap<Integer, Integer>();
for(int i=0; i<n; i++) {
indexMap.put(inorder[i], i);
}
return myBuildTree(preorder, inorder, 0, n-1, 0, n-1);
}
/** 递归函数 */
public TreeNode myBuildTree(int[] preorder, int inorder[], int preorder_left, int preorder_right, int inorder_left, int inorder_right){
if (preorder_left > preorder_right) {
return null;
}
// 前序遍历中的第一个结点就是根结点
int preorder_root = preorder_left;
// 在中序遍历中定位根节点
int inoreder_root = indexMap.get(preorder[preorder_root]);
// 先把根节点建立出来出来
TreeNode root = new TreeNode(preorder[preorder_root]);
// 得到左子树的结点数目
int size_left_subtree = inoreder_root - inorder_left;
// 递归的构造左子树,并链接到根节点
// 先序遍历从[左边界+1 开始的size_left_subtree]个元素对应了中序遍历从[左边界开始到根节点定位-1]的元素
root.left = myBuildTree(preorder, inorder, preorder_left+1, preorder_left+size_left_subtree, inorder_left, inoreder_root-1);
// 递归的构造右子树,并链接到根节点
// 先序遍历中, [从左边界+1+左子树结点数目开始,到有边界]的元素对应了中序遍历[从根节点定位+1 到 右边界]的元素
root.right = myBuildTree(preorder, inorder, preorder_left+size_left_subtree+1, preorder_right,inoreder_root+1, inorder_right);
return root;
}
}
103. 二叉树的锯齿形层序遍历
问题
代码
class Solution {
public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
List<List<Integer>> ans = new LinkedList<List<Integer>>();
if(root == null) { //
return ans;
}
Queue<TreeNode> nodeQueue = new ArrayDeque<TreeNode>(); // 创建一个双端队列
nodeQueue.offer(root); // 加入一个结点
boolean isOrderLeft = true; // 通过该标志控制从双端哪一端出队
while(!nodeQueue.isEmpty()) {
Deque<Integer> levelList = new LinkedList<Integer>();
int size = nodeQueue.size();
for(int i=0; i<size; i++) {
TreeNode curNode = nodeQueue.poll();
if(isOrderLeft) {
levelList.offerLast(curNode.val);
}else {
levelList.offerFirst(curNode.val);
}
if(curNode.left != null) {
nodeQueue.offer(curNode.left);
}
if(curNode.right != null) {
nodeQueue.offer(curNode.right);
}
}
ans.add(new LinkedList<Integer>(levelList));
isOrderLeft = !isOrderLeft;
}
return ans;
}
}
199. 二叉树的右视图
问题
思想(根->右->左的深度遍历法)
/**
深度优先遍历思想:
采用 根->右->左 的遍历顺序
当一个结点有 有左或者右子树的时候,深度加1,每次都会判断访问的当前结点是否等于深度
假如等于深度,则说明当前访问的结点为新的深度的第一个访问,又因为是根右左,所以访问的当前根为这一层最右侧结点
假如不封于深度,则说明,递归到某结点的左子树了,当前深度已有最右侧结点了
*/
代码
class Solution {
List<Integer> res = new ArrayList<>(); // 用于存储返回的最右侧结点
public List<Integer> rightSideView(TreeNode root) {
/**
深度优先遍历思想:
采用 根->右->左 的遍历顺序
当一个结点有 有左或者右子树的时候,深度加1,每次都会判断访问的当前结点是否等于深度
假如等于深度,则说明当前访问的结点为新的深度的第一个访问,又因为是根右左,所以访问的当前根为这一层最右侧结点
假如不封于深度,则说明,递归到某结点的左子树了,当前深度已有最右侧结点了
*/
dfs(root, 0); // 深度优先遍历(按照根->右->左的顺序)
return res;
}
private void dfs(TreeNode root, int depth) {
if(root == null) {
return;
}
// 先访问 当前结点,再递归访问右子树和左子树
if(depth == res.size()) { // 假如当前结点所在深度还没加入到res,则说明该结点为当前深度的最右侧
res.add(root.val);
}
depth++; // 深度+1, 注意这里下面两个左右子树的遍历,在这一时刻的深度都是一样的,并不会因为两次不同的遍历使得depth加两次
dfs(root.right, depth);
dfs(root.left, depth);
}
}
广度优先遍历思想
/**
广度优先遍历思想:
按队列层次遍历,每次遍历到一层的最后一个元素加入到返回的列表
*/
代码
class Solution {
public List<Integer> rightSideView(TreeNode root) {
/**
广度优先遍历思想:
按队列层次遍历,每次遍历到一层的最后一个元素加入到返回的列表
*/
List<Integer> res2 = new ArrayList<>();
if(root == null) { // 空树直接返回
return res;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root); // 先将根节点入队
while(!queue.isEmpty()) {// 当队列空时,说明层次遍历结束
int size = queue.size(); // 获取每层的结点数,因为每次遍历完一层,本层的结点就会出队,下一层的结点加入加入到队列
for(int i=0; i<size; i++) {
// 遍历本层的结点, 将有左右子树的子节点入队
TreeNode node = queue.poll();
if(node.left != null) {
// 左子树不空,入队
queue.offer(node.left);
}
if(node.right != null) {
// 右子树不空,入队
queue.offer(node.right);
}
if(i == size-1) {
// 访问的本层最后一个结点时, 一定是最右侧的结点,加入返回的集合
res2.add(node.val);
}
}
}
return res2;
}
113. 路径总和 II (二叉树路径问题汇总)
问题
思想
/**
算法思想:
深度优先遍历每个结点,当访问到叶子结点(无左右孩子)并且路径和为目标值(每次遍历时减去访问到的结点值)时加入到路径中
*/
代码
class Solution {
/**
算法思想:
深度优先遍历每个结点,当访问到叶子结点(无左右孩子)并且路径和为目标值(每次遍历时减去访问到的结点值)时加入到路径中
*/
List<List<Integer>> res = new LinkedList<List<Integer>>(); // 用于存放最终返回值
Deque<Integer> path = new LinkedList<Integer>(); // 用于暂存每个符合条件的路径
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
// 调用深度优先遍历算法
dfs(root, targetSum);
return res;
}
public void dfs(TreeNode root, int targetSum) {
if(root == null) {
// 结点为空为边界条件
return;
}
path.offerLast(root.val); //将访问的结点加入路径
targetSum -= root.val; //目标值减去访问到的结点值
if(root.left==null && root.right==null && targetSum==0) {
// 当该结点为叶子结点并且,到该叶子结点的路径和为目标值时,加入该路径到返回结果中
res.add(new LinkedList<Integer>(path));
}
// 左右递归遍历
dfs(root.left, targetSum);
dfs(root.right, targetSum);
path.pollLast(); // 将叶子结点弹出
}
}
450. 删除二叉搜索树中的节点
问题
思想
/**
算法思想:
1. 如果目标结点大于当前结点,则去右子树中寻找
2. 如果目标结点小于当前结点,则去左子树中寻找
3. 如果目标值就是当前结点,分如下情况:
3.1 无左子树,用右子树结点顶替删除的位置;
3.2 无右子树,用左子树结点顶替删除的位置;
3.3 其左右子树结点都有,则其左子树转移到右子树的最左结点的左子树树上
*/
代码
class Solution {
public TreeNode deleteNode(TreeNode root, int key) {
/**
算法思想:
1. 如果目标结点大于当前结点,则去右子树中寻找
2. 如果目标结点小于当前结点,则去左子树中寻找
3. 如果目标值就是当前结点,分如下情况:
3.1 无左子树,用右子树结点顶替删除的位置;
3.2 无右子树,用左子树结点顶替删除的位置;
3.3 其左右子树结点都有,则其左子树转移到右子树的最左结点的左子树树上
*/
if(root == null) {
return null;
}
if(key > root.val) {
// 删除的结点大于当前结点,向右寻找
root.right = deleteNode(root.right, key); // 因为在其右子树删除,所有右子树的根也可能会被删除
}else if(key < root.val) {
// 删除结点小于当前结点,向左寻找,同向右寻找同理
root.left = deleteNode(root.left, key);
}else {
// 当前结点就是删除结点
if(root.left == null) {
// 无左子树, 则其右子树的根就用来顶替
return root.right;
}else if(root.right == null) {
// 无右子树,则其左子树的根来顶替
return root.left;
}else{
// 左右子树都有
TreeNode node = root.right; // 保留要删除结点的右子树的根
while(node.left!=null) {
// 寻找到其右子树最左侧的结点
node = node.left;
}
node.left = root.left; // 用其右子树最左侧指向删除结点的左子树
root = root.right; // 删除结点的右子树的根成为新的树根
}
}
return root;
}
}