力扣刷题记录
写在最前,这个总结系列是比较早写的了,题目的类型不是很全,见解也不是很深,可以看我其他分开写的博客,是我后来在刷题中总结的;
一.数组
1.二分查找
1.1 二分查找模板
其中「二分」模板其实有三套
l < r
1.当check(mid) == true
调整的是 left = mid
时:计算 mid 的方式应该为:mid = left + right + 1>> 1
例如:
long l = 0, r = 1000009;
while (l < r) {
long mid = l + r + 1 >> 1;
if (check(mid)) {
l = mid;
} else {
r = mid - 1;
}
}
2.当check(mid) == true
调整的是right = mid
时:计算 mid 的方式应该为:mid = left + right >> 1
对应carl左闭右开情况 程序员carl
例如:
long l = 0, r = 1000009;
while (l < r) {
long mid = l + r >> 1;
if (check(mid)) {
r = mid;
} else {
l = mid + 1;
}
}
l <= r
3.一般情况,对应carl左闭右闭情况
while(left <= right){
int mid = (left + right) >> 1;
if(nums[mid] == target){
return mid;
}else if(nums[mid] < target){
left = mid + 1;
}else if(nums[mid] > target){
right = mid - 1;
}
}
注意事项
1.根据循环退出条件,决定最终返回值
eg:4中,退出时left == right,所以返回left和right都可以
2.搜索哪个范围,则使左右指针分别是此范围的左右边界。
例如:搜索范围是[l,h],则使left = l,right = h,保证搜索不会漏掉
1.2 二分查找递归和非递归
非递归
public static int binarySearch(int[] arr,int target){
int left = 0,right = arr.length - 1;
while (left <= right){
int mid = left + (right - left) / 2;
if (arr[mid] == target){
return mid;
}else if (arr[mid] < target){
left = mid + 1;
}else{
right = mid - 1;
}
}
return -1;
}
递归
//递归
public static int binarySearch(int left,int right,int[] arr,int target){
if (left> right){
return -1;
}
int mid = left + (right - left) / 2;
if (arr[mid] == target){
return mid;
}else if (arr[mid] < target){
return binarySearch(mid + 1,right,arr,target);
}else{
return binarySearch(left,mid - 1,arr,target);
}
}
1.3 二分查找(力扣704)
最简单的二分查找
public static int searchInsert(int[] nums, int target) {
int len = nums.length;
int left = 0,right = len - 1;
while (left <= right){
int mid = left + (right - left) / 2;
if (nums[mid] == target){
return mid;
}else if (nums[mid] < target){
left = mid + 1;
}else if (nums[mid] > target){
right = mid - 1;
}
}
return -1;
}
1.4搜索插入位置(力扣35)
//1.左闭右闭
public static int searchInsert(int[] nums, int target) {
int left = 0,right = nums.length - 1;
while(left <= right){
int mid = left + (right - left) / 2;
if (nums[mid] == target){
return mid;
}else if (nums[mid] < target){
left = mid + 1;
}else if (nums[mid] > target){
right = mid - 1;
}
}
//return right + 1 也是对的
return left;
}
//2.左闭右开
public static int searchInsert(int[] nums, int target) {
int left = 0,right = nums.length;
while (left < right){
int mid = left + (right - left) / 2;
if (nums[mid] == target){
return mid;
}else if (nums[mid] < target){
left = mid + 1;
}else if (nums[mid] > target){
right = mid;
}
}
return right;
}
1.5.在排序数组中查找元素的第一个和最后一个位置(力扣34)
//1.暴力
public static int[] searchRange(int[] nums, int target) {
int len = nums.length;
if (len == 0) {
return new int[]{-1, -1};
}
if (nums[0] > target || nums[len - 1] < target) {
return new int[]{-1, -1};
}
int left = -1, right = -1;
boolean flag = true;
for (int i = 0; i < len; i++) {
if (flag) {
if (nums[i] == target) {
left = i;
flag = false;
}
}
if (nums[i] == target) {
right = i;
}
}
return new int[]{left, right};
}
//2.二分
public static int[] searchRange(int[] nums, int target) {
int leftBorder = searchLeft(nums, target);
int rightBorder = searchRight(nums, target);
//数组中存在target
if (rightBorder - leftBorder >= 0) {
return new int[]{leftBorder, rightBorder};
}
//数组中不存在target && 数组长度为0
return new int[]{-1, -1};
}
public static int searchLeft(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] >= target) {
right = mid - 1;
} else if (nums[mid] < target) {
left = mid + 1;
}
}
return right + 1;
}
public static int searchRight(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] <= target) {
left = mid + 1;
}
}
return left - 1;
}
//3.二分优化
public static int[] searchRange(int[] nums, int target) {
//寻找左边界
int leftIndex = binarySearch(nums,target,true);
//寻找右边界
int rightIndex = binarySearch(nums,target,false) - 1;
//因为想要搜寻的目标值,有可能不在数组中,因此要对返回值进行判断
if (leftIndex <= rightIndex){
return new int[]{leftIndex,rightIndex};
}else{
return new int[]{-1,-1};
}
}
/*
* 1.寻找左边界,就是寻找第一个大于等于目标值的下标
* 2.寻找右边界,就是寻找第一个大于目标值的下标 - 1
* */
public static int binarySearch(int[] nums,int target,boolean lower){
//ans = nums.length,针对于 nums={1},target = 1的情况
int left = 0,right = nums.length - 1,ans = nums.length;
while (left <= right){
int mid = left + (right - left) / 2;
if (nums[mid] > target ||(lower && nums[mid] >= target)){
right = mid - 1;
ans = mid;
}else{
left = mid + 1;
}
}
return ans;
}
1.6.x 的平方根(力扣69)
//思路:找到第一个大于目标值的下标再-1
public static int mySqrt(int x) {
int left = 0,right = x;
while (left <= right){
int mid = left + (right - left) / 2;
//注意mid * mid 有可能超出int的范围
if ((long)mid * mid <= x){
left = mid + 1;
}else{
right = mid - 1;
}
}
return left - 1;
}
1.7.有效的完全平方数(力扣367)
//1.暴力,超出时间限制
public static boolean isPerfectSquare(int num) {
for (int i = 1; i * i <= num; i++) {
if (i * i == num){
return true;
}
}
return false;
}
//2.(还是超出时间限制)
public static boolean isPerfectSquare(int num) {
int subNum = 1;
while (num > 0) {
num -= subNum;
subNum += 2;
}
return num == 0;
}
//3.二分
public static boolean isPerfectSquare(int num) {
int left = 0,right = num;
while(left <= right){
int mid = left + (right - left) / 2;
long sum = mid * mid;
if (sum == num){
return true;
}else if (sum > num){
right = mid -1;
}else if (sum < num){
left = mid + 1;
}
}
return false;
}
//4.二分优化
public static boolean isPerfectSquare1(int num) {
if (num < 2) {
return true;
}
//num=2 num=3 直接返回false,因此right=num/2即可
long left = 2, right = num / 2, mid, sum;
while (left <= right) {
mid = left + (right - left) / 2;
sum = mid * mid;
if (sum == num) {
return true;
} else if (sum > num) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return false;
}
//5.牛顿迭代法
public static boolean isPerfectSquare2(int num) {
if (num < 2) {
return true;
}
long x = num / 2;
while (x * x > num) {
x = (x + num / x) / 2;
}
return (x * x == num);
}
3.寻找比目标字母大的最小字母(力扣744)
//自己想的
public static char nextGreatestLetter(char[] letters, char target) {
int left = 0,right = letters.length - 1;
char ans = letters[0];
while (left <= right){
int mid = left + (right - left) / 2;
if (letters[mid] <= target){
left = mid + 1;
}else{
ans = letters[mid];
right = mid - 1;
}
}
return ans;
}
2和3形成对比,一个要左边,一个要右边,注意比较代码的不同
4.有序数组中的单一元素(力扣540)
//3.二分法(四种情况。看官方解释)
public static int singleNonDuplicate(int[] nums) {
int left = 0,right = nums.length - 1;
while (left < right) {
int mid = left + (right - left) / 2;
//用来判断分成的两半是否为偶数,因为数组总个数必是奇数,分完之后两边个数一定相等
boolean halveAreEven = (right - mid) % 2 == 0;
if (nums[mid + 1] == nums[mid]) {
if (halveAreEven){
left = mid + 2;
}else{
right = mid - 1;
}
}else if (nums[mid - 1] == nums[mid]){
if (halveAreEven){
right = mid - 2;
}else{
left = mid + 1;
}
}else{
//恰好中间值就是出现一次的那个数
return nums[mid];
}
}
//因为退出时的情况时left == right,所以返回nums[left]和nums[right]都可以
return nums[left];
}
//4.二分法改进
/*
* 数组中只出现一次的元素,必然在下标为偶数的位置出现。
* 如果算出mid的下标不为偶数,则要将其下标变成偶数.
*
* 下标为偶数有两种情况:
* 1.nums[mid] == nums[mid + 1],那么此时nums[mid]前面的元素个数为偶数,这个单一元素必然在后面。
* 2.否则的话,单一元素要么是nums[mid],要么在nums[mid]的前面,所以right = mid;
* 退出时left == right,返回left和right都可以
* */
public static int singleNonDuplicate(int[] nums) {
int left = 0,right = nums.length - 1;
while (left < right){
int mid = left + (right - left) / 2;
if (mid % 2 != 0) mid --;
if (nums[mid] == nums[mid + 1]){
left = mid + 2;
}else{
right = mid;
}
}
return nums[left];
}
5.第一个错误的版本(力扣278)
/*
* 如果第 m 个版本出错,则表示第一个错误的版本在 [l, m] 之间,令 h = m;
* 否则第一个错误的版本在 [m + 1, h] 之间,令 l = m + 1。
因为 h 的赋值表达式为 h = m,因此循环条件为 l < h。
* */
public int firstBadVersion(int n) {
int left = 1,right = n;
while (left < right){
int mid = left + (right - left) / 2;
if (isBadVersion(mid) == false){
left = mid + 1;
}else{
right = mid;
}
}
return left;
}
6.寻找旋转排序数组中的最小值(力扣153)
//二分法,解释见官方,特别是图画的很清晰
public static int findMin(int[] nums) {
int left = 0,right = nums.length - 1;
while (left < right){
int mid = left + (right - left) / 2;
if (nums[mid] < nums[right]){
right = mid;
}else{
left = mid + 1;
}
}
return nums[left];
}
一.搜索
1.BFS
1.1总结
1.队列
2.while循环
1.2相同的树(力扣100)
//2.BFS(一般借助于队列)
/*
* 1.队列
* 2.while循环
* */
public boolean isSameTree(TreeNode p, TreeNode q) {
if (p == null || q == null) return p == q;
Queue<TreeNode> queue1 = new LinkedList<>();
Queue<TreeNode> queue2 = new LinkedList<>();
//先把根节点放到队列里
queue1.offer(p);
queue2.offer(q);
while (!queue1.isEmpty() && !queue2.isEmpty()){
TreeNode node1 = queue1.poll();
TreeNode node2 = queue2.poll();
if (node1.val != node2.val){
return false;
}
TreeNode left1 = node1.left,right1 = node1.right,left2 = node2.left,right2 = node2.right;
if (left1 == null ^ left2 == null){
return false;
}
if (right1 == null ^ right2 == null){
return false;
}
if (left1 != null){
queue1.offer(left1);
}
if (right1 != null){
queue1.offer(right1);
}
if (left2 != null){
queue2.offer(left2);
}
if (right2 != null){
queue2.offer(right2);
}
}
return queue1.isEmpty() && queue2.isEmpty();
}
2.DFS
2.1总结
1.写好判断条件
2.递归
2.2相同的树(力扣100)
//1.DFS(递归)
/*
* 1.写好判断条件
* 2.递归
* */
public boolean isSameTree(TreeNode p, TreeNode q) {
if (p == null || q == null) return p == q;
if (p.val != q.val) return false;
return isSameTree(p.left,q.left) && isSameTree(p.right,q.right);
}
3.回溯
1.回溯算法经验总结
1、最本质的法宝是“画图”,千万不能偷懒,拿纸和笔“画图”能帮助我们更好地分析递归结构,这个“递归结构”一般是“树形结构”,而符合题意的解正是在这个“树形结构”上进行一次“深度优先遍历”,这个过程有一个形象的名字,叫“搜索”;
我们写代码也几乎是“看图写代码”,所以“画树形图”很重要。
2、然后使用一个状态变量,一般我习惯命名为 path、pre ,在这个“树形结构”上使用“深度优先遍历”,根据题目需要在适当的时候把符合条件的“状态”的值加入结果集;
这个“状态”可能在叶子结点,也可能在中间的结点,也可能是到某一个结点所走过的路径。
3、在某一个结点有多个路径可以走的时候,使用循环结构。当程序递归到底返回到原来执行的结点时,“状态”以及与“状态”相关的变量需要“重置”成第 1 次走到这个结点的状态,这个操作有个形象的名字,叫“回溯”,“回溯”有“恢复现场”的意思:意即“回到当时的场景,已经走过了一条路,尝试走下一条路”。
第 2 点中提到的状态通常是一个列表结构,因为一层一层递归下去,需要在列表的末尾追加,而返回到上一层递归结构,需要“状态重置”,因此要把列表的末尾的元素移除,符合这个性质的列表结构就是“栈”(只在一头操作)。
4、当我们明确知道一条路走不通的时候,例如通过一些逻辑计算可以推测某一个分支不能搜索到符合题意的结果,可以在循环中 continue 掉,这一步操作叫“剪枝”。
“剪枝”的意义在于让程序尽量不要执行到更深的递归结构中,而又不遗漏符合题意的解。因为搜索的时间复杂度很高,“剪枝”操作得好的话,能大大提高程序的执行效率。
“剪枝”通常需要对待搜索的对象做一些预处理,例如第 47 题、第 39 题、第 40 题、第 90 题需要对数组排序。“剪枝”操作也是这一类问题很难的地方,有一定技巧性。
总结一下:“回溯” = “深度优先遍历” + “状态重置” + “剪枝”,写好“回溯”的前提是“画图”。
2.回溯算法模板(力扣77)
# 回溯搜索的模板
def backtrack(待搜索的集合, 递归到第几层, 状态变量 1, 状态变量 2, 结果集):
# 写递归函数都是这个套路:先写递归终止条件
if 可能是层数够深了:
# 打印或者把当前状态添加到结果集中
return
for 可以执行的分支路径 do
# 剪枝
if 递归到第几层, 状态变量 1, 状态变量 2, 符合一定的剪枝条件:
continue
对状态变量状态变量 1, 状态变量 2 的操作(#)
# 递归执行下一层的逻辑
backtrack(待搜索的集合, 递归到第几层, 状态变量 1, 状态变量 2, 结果集)
对状态变量状态变量 1, 状态变量 2 的操作(与标注了 # 的那一行对称,称为状态重置)
end for
三.快速乘法
1.快速乘法模板(力扣29)
//快速乘法(a为乘数,b为被乘数,sum为乘积)
public static int quickSum(int a,int b){
int sum = 0;
while (b > 0){
if ((b & 1) == 1){
sum += a;
}
a += a;
b = b >> 1;
}
return sum;
}
四.快速幂
1.快速幂模板
//快速幂(a为底,b为幂次)
public static int quickSum(int a,int b){
int sum = 1;
while (b > 0){
if ((b & 1) == 1){
sum *= a;
}
a *= a;
b = b >> 1;
}
return sum;
}
五.位运算
1.异或(力扣136)
异或运算有以下三个性质。
1.任何数和 0做异或运算,结果仍然是原来的数,即a⊕0=a
;
2.任何数和其自身做异或运算,结果是 0,即a⊕a=0
;
3.异或运算满足交换律和结合律,即a⊕b⊕a=b⊕a⊕a=b⊕(a⊕a)=b⊕0=b
;
public static int singleNumber(int[] nums) {
int first = 0;
for (int i : nums){
first ^= i;
}
return first;
}
2.汉明距离(力扣461)
//1.利用javaAPI
public static int hammingDistance(int x, int y) {
return Integer.bitCount(x ^ y);
}
//2.移位计算
public static int hammingDistance(int x, int y) {
int z = x ^ y;
int count = 0;
while (z > 0){
count += z & 1;
z >>= 1;
}
return count;
}
//3.Brian Kernighan 算法(只统计1的个数,略过0)
public static int hammingDistance(int x, int y) {
int z = x ^ y;
int count = 0;
while (z != 0){
z &= z - 1;
count ++;
}
return count;
}
3.丢失的数字(力扣136)
// 2.利用异或运算
public static int singleNumber(int[] nums) {
int first = 0;
for (int i : nums){
first ^= i;
}
return first;
}
4.只出现一次的数字(力扣268)
//1.数学方法
public static int missingNumber(int[] nums) {
int sum = 0;
for (int i : nums){
sum += i;
}
int len = nums.length;
return len * (1 + len) / 2 - sum;
}
//2.位运算
public static int missingNumber(int[] nums) {
int len = nums.length;
int sum = 0;
for (int i = 0; i < len; i++) {
sum ^= (i ^ nums[i]);
}
return sum ^ len;
}
5.只出现一次的数字 III(力扣260)
//1.哈希集合(最先想到的方法)
public static int[] singleNumber(int[] nums) {
Set<Integer> set = new HashSet<>();
for (int i : nums){
if (set.contains(i)){
set.remove(i);
}else{
set.add(i);
}
}
int[] res = new int[2];
int j = 0;
for (int i : set){
res[j ++] = i;
}
return res;
}
//2.位运算
public static int[] singleNumber(int[] nums) {
int nAdd = 0;
for (int num : nums){
nAdd ^= num;
}
int i = 1;
while ((i & nAdd) == 0){
i <<= 1;
}
int nAdd1 = 0,nAdd2 = 0;
for (int num : nums){
if ((i & num) != 0){
nAdd1 ^= num;
}else{
nAdd2 ^= num;
}
}
return new int[]{nAdd1,nAdd2}
}
6.颠倒二进制位(力扣190)
//1.利用javaAPI
public int reverseBits(int n) {
return Integer.reverse(n);
}
//2.逐位反转
public int reverseBits(int n) {
int rev = 0;
for (int i = 0; i < 32 && n != 0; i++) {
rev |= (n & 1) << (31 - i);
n >>>= 1;
}
return rev;
}
7.不用额外变量交换两个整数
给定两个整数a和b
a = a ^ a ^ b = b;
b = b ^ b ^ a = a;
a = a ^ b;
b = a ^ b;
a = a ^ b;
8.2 的幂(力扣231)
//1.自己想的
public static boolean isPowerOfTwo(int n) {
if (n <= 0){
return false;
}
if (Math.pow(2,30) % n == 0){
return true;
}
return false;
}
//2.技巧
public static boolean isPowerOfTwo(int n) {
return n > 0 && (n & (n - 1)) == 0;
}
//3.技巧
public static boolean isPowerOfTwo(int n) {
return n > 0 && (n & -n) == n;
}
//4.技巧
public static boolean isPowerOfTwo(int n) {
return n > 0 && Integer.bitCount(n) == 1;
}
9.4的幂(力扣342)
//1.使用循环
public static boolean isPowerOfFour(int n) {
while ( n > 0 && Integer.bitCount(n) == 1){
if ((n & 1) == 1){
return true;
}
n >>= 2;
}
return false;
}
//2.技巧
public static boolean isPowerOfFour(int n) {
//0xaaaaaaaa 和 0xAAAAAAAA 一样
return n > 0 && (n & (n - 1)) == 0 && (n & 0xaaaaaaaa) == 0;
}
//3.技巧
public static boolean isPowerOfFour(int n) {
return n > 0 && (n & (n - 1)) == 0 && n % 3 == 1;
}
10.交替位二进制数(力扣693 )
//1.自己想的
public static boolean hasAlternatingBits(int n) {
while (n > 0){
if ((n & 1) == (n >> 1 & 1)){
return false;
}
n >>= 1;
}
return true;
}
/*2.
* 分析:
* 如果n是交替的01,对于它右移一位后得到的m,
* 存在n跟m在二进制下必然是0和1对应的(对位)。异或运算必定都是1;
*
* 举个栗子:5=101 5>>1=10,5^(5>>1)=111
* 101
* 10 =111
*
* 其他情况都不会满足这个特征。所以temp=n^(n>>1)必定满足temp=2^N-1;
* 而temp+1后是N+1位二进制数2^(N+1)。
* 所以temp&(temp+1)==0;
* 如果满足这个等式就是就是交替位二进制数
*/
public static boolean hasAlternatingBits(int n) {
int temp=n^(n>>1);
return (temp&(temp+1))==0;
}
//3.
public static boolean hasAlternatingBits(int n) {
while (n > 0){
if ((n % 2 == n / 2 % 2)){
return false;
}
n /= 2;
}
return true;
}
11.数字的补数(力扣476)
/*
思路:
5 -> 101 , 其补码为 010 ,将 101 与 111 做 异或即可达到他对应的补码
问题的关键为:求掩码
*/
//1.自己想的(找规律)
/*
1 -> 1
11 -> 3
111 -> 7
1111 -> 15
...
* */
public static int findComplement(int num) {
int pre = 1,i = 1;
while (pre < num){
pre += Math.pow(2,i);
i ++;
}
return pre ^ num;
}
//2.思路和1一样
public static int findComplement(int num) {
int temp = num;
int pre = 0;
while(temp > 0){
temp >>= 1;
pre = (pre << 1) + 1;
}
return num ^ pre;
}
12.两整数之和(力扣371)
//1.找规律
public static int getSum(int a, int b) {
return (a ^ b) + ((a & b) << 1);
}
13.最大单词长度乘积(力扣318)
//1.位运算
public static int maxProduct(String[] words) {
int len = words.length;
int[] arr = new int[len];
for (int i = 0;i < len;i ++){
for (char c : words[i].toCharArray()){
/*
arr[i]记录的是第i个字符串中26个字母是否出现
abc -> 000111
def -> 111000
如果相与为0,则两个字符串中没有相同的字母
* */
arr[i] |= 1 << (c - 'a');
}
}
int max = 0;
for (int i = 0; i < len - 1; i++) {
for (int j = i + 1; j < len; j++) {
if ((arr[i] & arr[j]) == 0){
max = Math.max(max,words[i].length() * words[j].length());
}
}
}
return max;
}
14.比特位计数(力扣338)
//1.使用内置函数
public static int[] countBits(int n) {
int[] arr = new int[n + 1];
for (int i = 0; i <= n; i++) {
arr[i] = Integer.bitCount(i);
}
return arr;
}
//2.Brian Kernighan算法
public static int[] countBits(int n) {
int[] arr = new int[n + 1];
for (int i = 0;i <= n;i ++){
arr[i] = countOnes(i);
}
return arr;
}
public static int countOnes(int num){
int count = 0;
while (num != 0){
num &= num - 1;
count ++;
}
return count;
}
六.快慢指针
1.快慢指针思想
假想「乌龟」和「兔子」在链表上移动,「兔子」跑得快,「乌龟」跑得慢。当「乌龟」和「兔子」从链表上的同一个节点(有时不是同一个节点)开始移动时,如果该链表中没有环,那么「兔子」将一直处于「乌龟」的前方;如果该链表中有环,那么「兔子」会先于「乌龟」进入环,并且一直在环内移动。等到「乌龟」进入环时,由于「兔子」的速度快,它一定会在某个时刻与乌龟相遇,即套了「乌龟」若干圈。
快慢指针指两个移动速度不同的指针,多为2倍关系,快慢指针多用来解决链表问题
2.循环链表(力扣141-判断是否有环)
1.我们可以根据上述思路来解决问题。具体地,我们定义两个指针,一快一满。慢指针每次只移动一步,而快指针每次移动两步。初始时,慢指针在位置 head,而快指针在位置 head.next。这样一来,如果在移动的过程中,快指针反过来追上慢指针,就说明该链表为环形链表。否则快指针将到达链表尾部,该链表不为环形链表。
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null){
return false;
}
ListNode slow = head;
ListNode fast = head.next;
while (slow != fast){
if (fast == null || fast.next == null){
return false;
}
slow = slow.next;
//必须保证fast.next != null 才能继续迭代
fast = fast.next.next;
}
return true;
}
2.或者fast和slow初始都是head
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null){
return false;
}
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;
}
3.循环链表II(力扣142-寻找环入口)
根据推导(看题解解释)
如果链表中有环,当fast指针和slow指针第一次相遇后,让fast指针重新指向head,并将步长改为1,当两指针再次相遇时,相遇结点即为循环入口。
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 (slow == fast) {
break;
}
}
//调整fast指针
fast = head;
//再次相遇
while (fast != slow) {
fast = fast.next;
slow = slow.next;
}
return fast;
}
4. 链表的中间结点(力扣876)
public ListNode middleNode(ListNode head) {
ListNode slow = head,fast = head;
while (fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
5.链表中倒数第k个节点(剑指 Offer 22)
严格来说是双指针
/*
* 快指针先走k步,要到达链表末尾还需走(n-k)步;
* 此时慢指针从头结点开始移动,走(n-k)步,刚好到达倒数第k个节点。
* 总结:快指针先走k步瞧瞧,再和慢指针一起走
* */
public static ListNode getKthFromEnd(ListNode head, int k) {
ListNode slow = head,fast = head;
for (int i = 0; i < k; i++) {
fast = fast.next;
while (fast != null){
fast = fast.next;
slow = slow.next;
}
}
return slow;
}
6.相交链表(力扣160)
刷题时评论里好多人才,痛苦思考之余带来一点欢乐。
走到尽头见不到你,于是走过你来时的路,等到相遇时才发现,你也走过我来时的路。
/*
* 题解:设链表A的长度为a+c,链表B的长度为b+c。
* a为链表A不公共部分,b为链表B不公共部分,c为链表A、B的公共部分
*将两个链表连起来,A->B和B->A,长度:a+c+b+c=b+c+a+c,若链表AB相交,则a+c+b与b+c+a就会抵消,
* 它们就会在c处相遇;若不相交,则a+b=b+a,它们各自移动到尾部循环结束,即返回null
* */
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null){
return null;
}
ListNode node1 = headA,node2 = headB;
while (node1 != node2){
node1 = node1 == null ? headB:node1.next;
node2 = node2 == null ? headA:node2.next;
}
return node1;
}
七.哈希表(集合)
1.介绍
hash 的检索速度是非常快的。hash 的好处就是检索速度快,因为它添加数据的时候就早就做好你要检索的准备
哈希表中添加元素并计数
Map<Integer,Integer> map = new HashMap<>();
for (int i : nums){
if (!map.containsKey(i)){
map.put(i,1);
}
map.put(i,map.get(i) + 1);
}
遍历哈希表常用的一种方法
for (Map.Entry<Integer,Integer> entry : map.entrySet()){
if (entry.getValue() > maxCount){
maxNum = entry.getKey();
maxCount = entry.getValue();
}
}
2.循环链表(力扣141)
public boolean hasCycle(ListNode head) {
Set<ListNode> set = new HashSet<>();
ListNode node = head;
while (node != null){
//set的add方法返回类型为boolean
if (!set.add(node)){
return true;
}
node = node.next;
}
return false;
}
3.循环链表II(力扣142)
public ListNode detectCycle(ListNode head) {
Set<ListNode> set = new HashSet<>();
ListNode node = head;
while (node != null){
if (!set.add(node)){
return node;
}
node=node.next;
}
return null;
}
4.最长和谐子序列(力扣594)
//1.哈希表
public static int findLHS(int[] nums) {
Map<Integer,Integer> map = new HashMap<>();
for (int i : nums){
map.put(i,map.getOrDefault(i,0) + 1);
}
int max = 0;
for (int key : map.keySet()){
if (map.containsKey(key + 1)){
max = Math.max(max,map.get(key) + map.get(key + 1));
}
}
return max;
}
//2.枚举(超出时间限制)
public static int findLHS(int[] nums) {
int max = 0;
for (int i = 0; i < nums.length; i++) {
int count = 0;
//防止数组中的数都一样 这种情况
boolean flag = false;
for (int j = 0; j < nums.length; j++) {
if (nums[i] == nums[j]) {
count ++;
}
if (nums[j] == nums[i] + 1){
count ++;
flag = true;
}
if (flag){
max = Math.max(max,count);
}
}
}
return max;
}
5.两数之和(力扣1)
//1.哈希表
public static int[] twoSum(int[] nums, int target) {
Map<Integer,Integer> map = new HashMap<>();
for (int i = 0;i < nums.length; i ++){
if (map.containsKey(target - nums[i])){
return new int[]{map.get(target - nums[i]),i};
}
map.put(nums[i],i);
}
return new int[0];
}
6.存在重复元素(力扣217)
//1.暴力解法,时间复杂度较高
public static boolean containsDuplicate(int[] nums) {
for (int i = 0; i < nums.length; i++) {
for (int j = i + 1; j < nums.length; j++) {
if (nums[i] == nums[j]){
return true;
}
}
}
return false;
}
//2.在对数字从小到大排序之后,数组的重复元素一定出现在相邻位置中。
// 因此,我们可以扫描已排序的数组,每次判断相邻的两个元素是否相等,
// 如果相等则说明存在重复的元素。
public static boolean containsDuplicate1(int[] nums) {
Arrays.sort(nums);
for (int i = 0; i < nums.length - 1; i++) {
if (nums[i] == nums[i + 1]){
return true;
}
}
return false;
}
//3.哈希集合
public static boolean containsDuplicate1(int[] nums) {
Set<Integer> set = new HashSet<>();
for (int i : nums){
if (!set.add(i)){
return true;
}
// if (set.contains(i)){
// return true;
// }
// set.add(i);
}
return false;
}
7.最长连续序列(力扣128)
//1.哈希集合(超出时间限制)
public static int longestConsecutive(int[] nums) {
Set<Integer> set = new HashSet<>();
for (int i : nums){
set.add(i);
}
int max = 0;
for (int i = 0; i < nums.length; i++) {
int curNum = nums[i];
int count = 1;
while (set.contains(curNum + 1)){
count ++;
curNum ++;
}
max = Math.max(max,count);
}
return max;
}
//2.哈希集合优化版
public static int longestConsecutive(int[] nums) {
Set<Integer> set = new HashSet<>();
for (int i : nums){
set.add(i);
}
int max = 0;
for (int i = 0; i < nums.length; i++) {
int curNum = nums[i];
//这里做了优化
if (!set.contains(curNum - 1)){
int count = 1;
while (set.contains(curNum + 1)){
count ++;
curNum ++;
}
max = Math.max(max,count);
}
}
return max;
}
八.动态规划
1.找好初始状态
几维数组;数组每一维的含义以及数组本身的含义;数组的初始状态
2.状态转移方程
条件;边界条件
3.返回值
确定返回值
4.空间优化
// 在动态规划中,如果第i个状态只与第i-1个状态有关,而不与其他的例如第i - k(0 < k < i)个状态有关,那么意味着此时在空间上有优化的空间,我们可以采用滚动数组或者从后往前的方式填表来代替开辟更高维度的数组。
1.股票问题
1.买卖股票的最佳时机(力扣121)
/*
* dp[i]表示截止到第i天,价格的最低点是多少
* dp[i]=min(dp[i-1],prices[i])
* */
public static int maxProfit(int[] prices) {
int[] dp = new int[prices.length];
int max = 0;
dp[0] = prices[0];
for (int i = 1; i < prices.length; i++) {
dp[i] = dp[i - 1] < prices[i] ? dp[i - 1]:prices[i];
max = (prices[i] - dp[i]) > max?(prices[i] - dp[i]):max;
}
return max;
}
接着考虑优化空间,仔细观察动态规划的辅助数组,其每一次只用到了dp[i-1]
这一个空间,因此可以把数组改成单个变量来存储截止到第i天的价格最低点。优化之后的代码:
/*
* 固定一天卖出,同时记录已经遍历过的数组元素中的最小值
* */
public static int maxProfit(int[] prices) {
int minPrice = Integer.MAX_VALUE,max = 0;
for (int i = 0; i < prices.length; i++) {
if (prices[i] < minPrice){
minPrice = prices[i];
}
int res = prices[i] - minPrice;
if (res > max){
max = res;
}
}
return max;
}
2.买卖股票的最佳时机 II(力扣122)
/*1.动态规划
定义状态dp[i][0]表示第i天交易完后手里没有股票的最大利润,
dp[i][1]表示第i天交易完后手里持有一支股票的最大利润(i从0开始)
状态转移方程:
dp[i][0] = Math.max(dp[i - 1][0],dp[i - 1][1] + prices[i]);
dp[i][1] = Math.max(dp[i - 1][1],dp[i - 1][0] - prices[i]);
* */
public static int maxProfit(int[] prices) {
int len = prices.length;
int[][]dp = new int[len][2];
dp[0][0] = 0;
dp[0][1] = -prices[0];
for (int i = 1; i < len; i++) {
dp[i][0] = Math.max(dp[i - 1][0],dp[i - 1][1] + prices[i]);
dp[i][1] = Math.max(dp[i - 1][1],dp[i - 1][0] - prices[i]);
}
return dp[len - 1][0];
}
注意到上面的状态转移方程中,每一天的状态只与前一天的状态有关,而与更早的状态都无关,因此我们不必存储这些无关的状态,只需要将dp[i−1][0]
和dp[i−1][1]
存放在两个变量中
public static int maxProfit(int[] prices) {
int len = prices.length;
int dp0 = 0,dp1 = -prices[0];
for (int i = 1; i < len; i++) {
dp0 = Math.max(dp0,dp1 + prices[i]);
dp1 = Math.max(dp1,dp0 - prices[i]);
}
return dp0;
}
因为交易次数不受限,如果可以把所有的上坡全部收集到,一定是利益最大化的
public static int maxProfit(int[] prices) {
int len = prices.length;
if (len == 1){
return 0;
}
int ans = 0;
for (int i = 1; i < len; i++) {
if (prices[i] - prices[i - 1] > 0){
ans += (prices[i] - prices[i - 1]);
}
}
return ans;
}
3.最佳买卖股票时机含冷冻期(力扣309)
为便于理解,首先分为四种状态
// 1.动态规划
public static int maxProfit(int[] prices) {
int len = prices.length;
int[][] dp = new int[len][4];
dp[0][0]=0;//不持有股票,当天没卖出
dp[0][1]=0;//不持有股票,当天卖出
dp[0][2]=-1*prices[0];//持有股票,当天买入;
dp[0][3]=-1*prices[0];//持有股票,非当天买入的;
for (int i = 1; i < len; i++) {
dp[i][0] = Math.max(dp[i - 1][0],dp[i - 1][1]);
dp[i][1] = Math.max(dp[i - 1][2] + prices[i],dp[i - 1][3] + prices[i]);
dp[i][2] = dp[i - 1][0] - prices[i];
dp[i][3] = Math.max(dp[i - 1][3],dp[i - 1][2]);
}
return Math.max(dp[len - 1][0],dp[len - 1][1]);
}
接着分析,我们发现可以将持有股票的情况归为一种状态
// 2.动态规划(持有股票的情况归在一起)
public static int maxProfit(int[] prices) {
int len = prices.length;
int[][] dp = new int[len][4];
dp[0][0]=0;//不持有股票,当天没卖出
dp[0][1]=0;//不持有股票,当天卖出
dp[0][2]=-prices[0];//持有股票;
for (int i = 1; i < len; i++) {
dp[i][0] = Math.max(dp[i - 1][0],dp[i - 1][1]);
dp[i][1] = dp[i - 1][2] + prices[i];
dp[i][2] = Math.max(dp[i - 1][0] - prices[i],dp[i - 1][2]);
}
return Math.max(dp[len - 1][0],dp[len - 1][1]);
}
4.买卖股票的最佳时机含手续费(力扣714)
和第2个股票问题一样,不过是加了手续费
这一步不一样:
dp[i][0] = Math.max(dp[i - 1][0],dp[i - 1][1] + prices[i] - fee);
// 1.动态规划
public static int maxProfit(int[] prices, int fee) {
int len = prices.length;
int[][] dp = new int[len][2];
dp[0][0] = 0;
dp[0][1] = -prices[0];
for (int i = 1; i < len; i++) {
dp[i][0] = Math.max(dp[i - 1][0],dp[i - 1][1] + prices[i] - fee);
dp[i][1] = Math.max(dp[i - 1][1],dp[i - 1][0] - prices[i]);
}
return dp[len - 1][0];
}
依然可以空间优化
// 2.动态规划空间优化
public static int maxProfit(int[] prices, int fee) {
int len = prices.length;
int dp0 = 0;
int dp1 = -prices[0];
for (int i = 1; i < len; i++) {
dp0 = Math.max(dp0,dp1 + prices[i] - fee);
dp1 = Math.max(dp1,dp0 - prices[i]);
}
return dp0;
}
5.买卖股票的最佳时机 III(力扣123)
具体解释看官方
public static int maxProfit(int[] prices) {
int buy1 = -prices[0];
int sell1 = 0;
int buy2 = -prices[0];
int sell2 = 0;
for (int i = 1; i < prices.length; i++) {
buy1 = Math.max(buy1,-prices[i]);
sell1 = Math.max(sell1,buy1 + prices[i]);
buy2 = Math.max(buy2,sell1 - prices[i]);
sell2 = Math.max(sell2,buy2 + prices[i]);
}
return sell2;
}
6.买卖股票的最佳时机 IV(力扣188)
具体解释看官方
public static int maxProfit(int k, int[] prices) {
if (prices.length == 0) {
return 0;
}
int n = prices.length;
k = Math.min(k, n / 2);
int[][] buy = new int[n][k + 1];
int[][] sell = new int[n][k + 1];
buy[0][0] = -prices[0];
sell[0][0] = 0;
for (int i = 1; i <= k; ++i) {
buy[0][i] = sell[0][i] = Integer.MIN_VALUE / 2;
}
for (int i = 1; i < n; ++i) {
buy[i][0] = Math.max(buy[i - 1][0], sell[i - 1][0] - prices[i]);
for (int j = 1; j <= k; ++j) {
buy[i][j] = Math.max(buy[i - 1][j], sell[i - 1][j] - prices[i]);
sell[i][j] = Math.max(sell[i - 1][j], buy[i - 1][j - 1] + prices[i]);
}
}
return Arrays.stream(sell[n - 1]).max().getAsInt();
}
2.斐波那契数列
1.爬楼梯(力扣70)
很简单
public static int climbStairs(int n) {
int[] dp = new int[n + 1];
//1.初始状态
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= n ; i++) {
//2.状态转移方恒
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
2.打家劫舍(力扣198)
public static int rob(int[] nums) {
int len = nums.length;
if (nums.length == 1){
return nums[0];
}
// 用dp[i]表示前i间房屋能偷窃到的最高总金额
int[] dp = new int[len];
dp[0] = nums[0];
dp[1] = Math.max(nums[0],nums[1]);
for (int i = 2; i < len; i++) {
dp[i] = Math.max(dp[i - 2] + nums[i],dp[i - 1]);
}
return dp[len - 1];
}
3.打家劫舍 II(力扣213)
具体解释看官方
public int rob(int[] nums) {
int length = nums.length;
if (length == 1) {
return nums[0];
} else if (length == 2) {
return Math.max(nums[0], nums[1]);
}
return Math.max(robRange(nums, 0, length - 2), robRange(nums, 1, length - 1));
}
public int robRange(int[] nums, int start, int end) {
int first = nums[start], second = Math.max(nums[start], nums[start + 1]);
for (int i = start + 2; i <= end; i++) {
int temp = second;
second = Math.max(first + nums[i], second);
first = temp;
}
return second;
}
3.矩阵路径
1.最小路径和(力扣64)
由于路径的方向只能是向下或向右,因此
1.网格的第一行的每个元素只能从左上角元素开始向右移动到达
2.网格的第一列的每个元素只能从左上角元素开始向下移动到达
此时的路径是唯一的,因此每个元素对应的最小路径和即为对应的路径上的数字总和。
for (int i = 1; i < m; i++) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
for (int i = 1; i < n; i++) {
dp[0][i] = dp[0][i - 1] + grid[0][i];
}
对于不在第一行和第一列的元素,可以从其上方相邻元素向下移动一步到达,或者从其左方相邻元素向右移动一步到达,元素对应的最小路径和等于其上方相邻元素与其左方相邻元素两者对应的最小路径和中的最小值加上当前元素的值。dp[i][j] = Math.min(dp[i - 1][j] + grid[i][j],dp[i][j - 1] + grid[i][j]);
//动态规划
/*
* dp[i][j]表示到第i行第j列的最小路径和。
* */
public static int minPathSum(int[][] grid) {
int m = grid.length;
int n = grid[0].length;
int[][] dp = new int[m][n];
dp[0][0] = grid[0][0];
//这里没想到(因为只能向下或向右移动)
for (int i = 1; i < m; i++) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
for (int i = 1; i < n; i++) {
dp[0][i] = dp[0][i - 1] + grid[0][i];
}
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = Math.min(dp[i - 1][j] + grid[i][j],dp[i][j - 1] + grid[i][j]);
}
}
return dp[m - 1][n - 1];
}
2.不同路径(力扣62)
dp[i][j]
表示到第i行,第j列的不同路径总数
因为只能向下或向右移动,可以得到状态转移方程
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
//1.动态规划
public static int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
for (int i = 0; i < m ; i++) {
dp[i][0] = 1;
}
for (int i = 0; i < n; i++) {
dp[0][i] = 1;
}
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m - 1][n - 1];
}
因为第i行的状态只和第i行,第i-1行的状态相关,或者第j列的状态只和第j列,第j-1列的状态相关,选择m,n中的小者创建数组,空间复杂度降为 O(min(m,n))
//2.动态规划空间优化
public static int uniquePaths(int m, int n) {
int min = Math.min(m,n);
int max = Math.max(m,n);
int[] dp = new int[min];
for (int i = 0; i < min; i++) {
dp[i] = 1;
}
for (int i = 1; i < max; i++) {
for (int j = 1; j < min; j++) {
dp[j] = dp[j] + dp[j - 1];
}
}
return dp[min - 1];
}
同时还有一种数学解法,见官方(m+n−2)(m+n−3)⋯n/(m−1)!
//3.数学解法
public static int uniquePaths(int m, int n) {
long ans = 1;
for (int i = n,j = 1; j < m; i++,j ++) {
ans = ans * i / j;
}
return (int) ans;
}
4.数组区间和
1.区域和检索 - 数组不可变(力扣303)
数组区间和等于前缀和的差
//303. 区域和检索 - 数组不可变
public class ThreeHundredThree {
// 1.利用前缀和
int[] sums;
public NumArray(int[] nums) {
int len = nums.length;
sums = new int[len + 1];
for (int i = 0; i < len; i++) {
sums[i + 1] = sums[i] + nums[i];
}
}
public int sumRange(int left, int right) {
return sums[right + 1] - sums[left];
}
为节省空间,如果数组可变的话,可以在原数组基础上进行操作
int[] nums;
public NumArray(int[] nums) {
for(int i = 1;i < nums.length;i ++){
nums[i] += nums[i - 1];
}
this.nums = nums;
}
public int sumRange(int left, int right) {
return left == 0 ? nums[right]:nums[right] - nums[left - 1];
}
2.等差数列划分(力扣413)
从小的例子出发,仔细观察,会发现当整个数组为(1, 2, 3, 4, 5, 6)的时候,我们先取出前三个,(1, 2, 3)的等差数列的个数为1,(1, 2, 3, 4)的等差数列的个数为3,(1, 2, 3, 4, 5)的等差数列的个数为6,(1, 2, 3, 4, 5, 6)的等差数列个数为10,以此类推我们可以很容易的发现在一个等差数列中加入一个数字,如果还保持着等差数列的特性,每次的增量都会加1,如果刚加进来的数字与原先的序列构不成等差数列,就将增量置为0,接下来继续循环,执行以上的逻辑即可.可以发现,这道题只要找到规律还是相当的简单的
//1.动态规划
public static int numberOfArithmeticSlices(int[] nums) {
int[] dp = new int[nums.length];
int sum = 0;
for (int i = 2; i < nums.length; i++) {
if ((nums[i] - nums[i - 1]) == (nums[i - 1] - nums[i - 2])){
dp[i] = dp[i - 1] + 1;
sum += dp[i];
}
}
return sum;
}
进行空间优化
//2.动态规划空间优化
public static int numberOfArithmeticSlices(int[] nums) {
int dp = 0;
int sum = 0;
for (int i = 2; i < nums.length; i++) {
if ((nums[i] - nums[i - 1]) == (nums[i - 1] - nums[i - 2])){
dp = dp + 1;
sum += dp;
}else{
dp = 0;
}
}
return sum;
}
5.分割整数
1.整数拆分(力扣343)
解释见官方
public static int integerBreak(int n) {
int[] dp = new int[n + 1];
dp[0] = 0;
dp[1] = 0;
for (int i = 2; i <= n; i++) {
int cur = 0;
for (int j = 1; j < i; j++) {
cur = Math.max(cur, Math.max(dp[i - j] * j,(i - j) * j));
}
dp[i] = cur;
}
return dp[n];
}
找规律(类似于剑指剪绳子的问题)
按3拆分,当剩余数等于1时,和前面的3合并为4,剩余数为2时,直接相乘。
//2.找规律
/*
* n 拆分
* 1 0
* 2 1+1
* 3 1+2
* 4 2+2
* 5 3+2
* 6 3+3
* 7 3+4
* 8 3+3+2
* 9 3+3+3
* 10 3+3+4
* 11 3+3+3+2
* ...
* */
public static int integerBreak(int n) {
if (n <= 3) return n - 1;
int a = 1;
while (n > 4){
n -= 3;
a *= 3;
}
return a * n;
}
2.完全平方数(力扣279)
//1.动态规划
/*找规律
* n sum
* 0 0
* 1 1
* 2 2
* 3 3
* 4 1
* 5 2
* 6 3
* 7 4
* 8 2
* 9 1
* 10 2
* 11 3
* 12 3
*
* ...
*
* 假设dp[i]表示整数i中包含的整数和的最小个数
*
* 根据以上罗列,可以发现以下规律:
* ...
* dp[3] = min{dp[3 - 1^2] + 1};
* dp[4] = min{dp[4 - 1^2] + 1 , dp[4 - 2^2] + 1};
* dp[5] = min{dp[5 - 1^2] + 1 , dp[5 - 2^2] + 1};
* ...
* dp[10] = min{dp[10 - 1^2] + 1 , dp[10 - 2^2] + 1 , dp[10 - 3^2] + 1}
* ...
*
* 将小于等于i的平方数存到数组square中,则当i >= square[j]时,
* dp[i] = Math.min(dp[i],dp[i - square[j]] + 1);(因为dp[i]在不断更新)
* */
public static int numSquares(int n) {
int[] dp = new int[n + 1];
Arrays.fill(dp, Integer.MAX_VALUE);
int max_index = (int) Math.sqrt(n) + 1;
int[] square = new int[max_index];
for (int i = 1; i < max_index; i++) {
square[i] = i * i;
}
dp[0] = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j < max_index; j++) {
if (i < square[j]){
break;
}
dp[i] = Math.min(dp[i],dp[i - square[j]] + 1);
}
}
return dp[n];
}
3.解码方法(力扣91)
//1.动态规划
/*
* dp[i]表示到字符串第i个数有多少种情况
*
* 那么每增加一位数,有两种情况
* 1.这一位单独算,如果不为0,则dp[i] = dp[i - 1];
* 2.这一位和它的前一位一起算,则dp[i] = dp[i - 2],
* 同时满足前一位数不为0,并且两位数组成的两位数小于或等于26
* 最终dp[i]等于两种情况的总和
*
* 以12为例,dp[2] = 2,dp[1] = 1;
* 如果新增加的数为3
* 1.单独算,dp[3] = dp[2];
* 2.一起算,dp[3] = dp[1];
* 所以dp[3] = 2 + 1 = 3
* */
public static int numDecodings(String s) {
int len = s.length();
int[] dp = new int[len + 1];
dp[0] = 1;
for (int i = 1; i <= len; i++) {
if (s.charAt(i - 1) != '0') {
dp[i] += dp[i - 1];
}
if (i > 1 && s.charAt(i - 2) != '0' && (((s.charAt(i - 2) - '0') * 10 + (s.charAt(i - 1) - '0')) <= 26)){
dp[i] += dp[i - 2];
}
}
return dp[len];
}
同样可以进行空间优化,令a = dp[i - 2],b = dp[i - 1],c = dp[i]
//2.动态规划(空间优化)
public static int numDecodings(String s) {
int len = s.length();
//a = dp[i - 2],b = dp[i - 1],c = dp[i]
int a = 0, b = 1, c = 0;
for (int i = 1; i <= len; i++) {
c = 0;
if (s.charAt(i - 1) != '0') {
c += b;
}
if (i > 1 && s.charAt(i - 2) != '0' && (((s.charAt(i - 2) - '0') * 10 + (s.charAt(i - 1) - '0')) <= 26)) {
c += a;
}
a = b;
b = c;
}
return c;
}
6.最长递增子序列
1.最长递增子序列(力扣300)
//1.动态规划
public static int lengthOfLIS(int[] nums) {
//定义dp[i]为考虑前i个元素,以第i个数字结尾的最长上升子序列的长度,
// 注意nums[i] 必须被选取
int[] dp = new int[nums.length];
//有一个数字时为1
dp[0] = 1;
int max = 1;
for (int i = 1; i < nums.length; i++) {
//当加入一个数字进行比较时,最短为这个数字本身,所以为1
dp[i] = 1;
//第二个for循环:每一轮找到的都是以第i个数字结尾的最长上升子序列的长度
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]){
dp[i] = Math.max(dp[i] , dp[j] + 1);
}
//因为以数组中最后一个数字结尾的最长上升子序列的长度不一定是最长的
//所以用max记录每一轮里最长长度,比较完所有轮,则为最长的
max = Math.max(max,dp[i]);
}
}
return max;
}
2.最长数对链(力扣646)
//1.动态规划(思路和300差不多)
public static int findLongestChain(int[][] pairs) {
//按照数对第一个元素大小升序排列
Arrays.sort(pairs, (a, b) -> a[0] - b[0]);
int len = pairs.length;
//dp[i]表示以第i个数对结尾(包含第i个数对)的前i个数对中的最大个数
int[] dp = new int[len];
Arrays.fill(dp,1);
for (int i = 1; i < len; i++) {
for (int j = 0; j < i; j++) {
if (pairs[i][0] > pairs[j][1]){
dp[i] = Math.max(dp[i],dp[j] + 1);
}
}
}
//因为pairs已经是有序的了,所以返回dp[len - 1]即可
return dp[len - 1];
}
3.摆动序列(力扣376)
解释见官方
//1.动态规划
public static int wiggleMaxLength(int[] nums) {
int len = nums.length;
if (len < 2){
return 1;
}
int[] up = new int[len];
int[] down = new int[len];
up[0] = down[0] = 1;
for (int i = 1; i < len; i++) {
if (nums[i] > nums[i - 1]){
up[i] = Math.max(up[i - 1],down[i -1] + 1);
down[i] = down[i - 1];
}else if (nums[i] < nums[i - 1]){
up[i] = up[i - 1];
down[i] = Math.max(down[i - 1],up[i - 1] + 1);
}else{
up[i] = up[i - 1];
down[i] = down[i - 1];
}
}
return Math.max(up[len - 1],down[len - 1]);
}
//2.动态规划空间优化
/*
* 注意到方法一中,我们仅需要前一个状态来进行转移,
* 所以我们维护两个变量即可
* */
public static int wiggleMaxLength(int[] nums) {
int len = nums.length;
if (len < 2){
return 1;
}
int up = 1,down = 1;
for (int i = 1; i < len; i++) {
if (nums[i] > nums[i - 1]){
up = Math.max(up,down + 1);
}else if (nums[i] < nums[i - 1]){
down = Math.max(down,up + 1);
}
}
return Math.max(up,down);
}
//3.动态规划空间优化
/*
* 注意到每有一个「峰」到「谷」的下降趋势,down值才会增加,
* 每有一个「谷」到「峰」的上升趋势,up 值才会增加。
* 且过程中down 与 up 的差的绝对值恒不大于1,即
* down <= up + 1,up <= down + 1;
* */
public static int wiggleMaxLength(int[] nums) {
int len = nums.length;
if (len < 2){
return 1;
}
int up = 1,down = 1;
for (int i = 1; i < len; i++) {
if (nums[i] > nums[i - 1]){
up = down + 1;
}else if (nums[i] < nums[i - 1]){
down = up + 1;
}
}
return Math.max(up,down);
}
7.最长公共子序列(力扣1143)
解释见官方
public static int longestCommonSubsequence(String text1, String text2) {
int m = text1.length(),n = text2.length();
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m ; i++) {
for (int j = 1; j <= n ; j++) {
if (text1.charAt(i - 1) == text2.charAt(j - 1)){
dp[i][j] = dp[i - 1][j - 1] + 1;
}else{
dp[i][j] = Math.max(dp[i - 1][j],dp[i][j - 1]);
}
}
}
return dp[m][n];
}
腾讯面试题:要求返回子序列
在已经求得最长公共子序列长度的基础上,从后往前依次寻找
public static String longestCommonSubsequence(String text1, String text2) {
int m = text1.length(),n = text2.length();
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m ; i++) {
for (int j = 1; j <= n ; j++) {
if (text1.charAt(i - 1) == text2.charAt(j - 1)){
dp[i][j] = dp[i - 1][j - 1] + 1;
}else{
dp[i][j] = Math.max(dp[i - 1][j],dp[i][j - 1]);
}
}
}
// return dp[m][n];
//腾讯面试题:并返回最长子序列
StringBuilder sb = new StringBuilder();
for (int i = m,j = n;dp[i][j] >= 1;) {
if (text1.charAt(i - 1) == text2.charAt(j - 1)){
sb.append(text1.charAt(i - 1));
i --;j --;
}else if (dp[i - 1][j] >= dp[i][j - 1]) {
i--;
}else{
j --;
}
}
sb.reverse();
String res = sb.toString();
return res.length() == 0 ? "-1" : res;
}
8.0-1背包问题
1.分割等和子集(力扣416)
总体思想:
做这道题需要做一个等价转换:是否可以从输入数组中挑选出一些正整数,使得这些数的和 等于 整个数组元素的和的一半。
解释见官方
//1.动态规划(dp[0][0] = false时)
public static boolean canPartition(int[] nums) {
int len = nums.length;
int sum = 0;
for (int i : nums){
sum += i;
}
int target = sum / 2;
if ((sum & 1) == 1){
return false;
}
boolean[][] dp = new boolean[len][target + 1];
if (nums[0] <= target){
dp[0][nums[0]] = true;
}
for (int i = 1; i < len; i++) {
for (int j = 0; j <= target; j++) {
dp[i][j] = dp[i - 1][j];
if (nums[i] == j){
dp[i][j] = true;
continue;
}
if (nums[i] < j){
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i]];
}
}
}
return dp[len - 1][target];
}
//2.动态规划(dp[0][0] = true时)
public static boolean canPartition(int[] nums) {
int len = nums.length;
int sum = 0;
for (int i : nums){
sum += i;
}
int target = sum / 2;
//如果和为奇数,不可能平分
if ((sum & 1) == 1){
return false;
}
//dp[i][j]表示从数组的[0, i] 这个子区间内挑选一些正整数,
//每个数只能用一次,使得这些数的和恰好等于j
boolean[][] dp = new boolean[len][target + 1];
dp[0][0] = true;
if (nums[0] <= target){
dp[0][nums[0]] = true;
}
for (int i = 1; i < len; i++) {
for (int j = 0; j <= target; j++) {
//最基本的,继承上一行这一列的结果
dp[i][j] = dp[i - 1][j];
//条件:如果新加入的数字刚好等于前i个数字的和减去前i-1个数字的和
//如果条件满足,更新dp[i][j],否则维持原状不变
if (nums[i] <= j){
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i]];
}
//如果刚加入的这个数字刚好等于总和的一半,直接满足条件
//例如列表中第三行最后一列时
if (dp[i][target]){
return true;
}
}
}
return dp[len - 1][target];
}
自己画个表格会清晰很多
//3.动态规划空间优化
public static boolean canPartition(int[] nums) {
int len = nums.length;
int sum = 0;
for (int i : nums){
sum += i;
}
if ((sum & 1) == 1){
return false;
}
int target = sum / 2;
boolean [] dp = new boolean[target + 1];
dp[0] = true;
if (nums[0] <= target){
dp[nums[0]] = true;
}
for (int i = 1; i < len; i++) {
for (int j = target;nums[i] <= j; j--) {
if (dp[target]){
return true;
}
dp[j] = dp[j] || dp[j - nums[i]];
}
}
//从前往后
// for (int i = 1; i < len; i++) {
// for (int j = 0;j < target; j++) {
//
// if (nums[i] <= j){
// dp[j] = dp[j] || dp[j - nums[i]];
// }
//
// if (dp[target]){
// return true;
// }
//
// }
//
// }
return dp[target];
}
从后往前
从前往后
到第二行的时候已经出现了问题(原因就是从前往后更新dp[j]的时候,会使dp[j - nums[i]发生变化,自己上手推一遍就明白了)
2.目标和(力扣494)
解释见官方
//1.动态规划
public static int findTargetSumWays(int[] nums, int target) {
int sum = 0;
for (int i : nums) {
sum += i;
}
if (Math.abs(sum) < Math.abs(target)){
return 0;
}
int len = nums.length;
int t = sum * 2 + 1;
int[][] dp = new int[len][t];
//注意nums[0] == 0的特殊情况
if (nums[0] == 0){
dp[0][sum] = 2;
}else{
dp[0][sum + nums[0]] = 1;
dp[0][sum - nums[0]] = 1;
}
//初始状态也可以这样写,这样当nums[0] == 0 时,为2
// dp[0][sum + nums[0]] ++;
// dp[0][sum - nums[0]] ++;
for (int i = 1; i < len; i++) {
for (int j = 0; j < t; j++) {
//不能超出左边界
int l = (j - nums[i]) > 0 ? j - nums[i] : 0;
//不能超出右边界
int r = (j + nums[i]) < t ? j + nums[i] : 0;
dp[i][j] = dp[i - 1][l] + dp[i - 1][r];
}
}
return dp[len - 1][sum + target];
}
3.一和零(力扣474)
解释见官方
//1.动态规划
public int findMaxForm(String[] strs, int m, int n) {
int len = strs.length;
int[][][] dp = new int[len + 1][m + 1][n + 1];
for (int i = 1; i <= len ; i++) {
int[] count = calcuZeroAndOne(strs[i - 1]);
for (int j = 0; j <= m ; j++) {
for (int k = 0; k <= n ; k++) {
dp[i][j][k] = dp[i - 1][j][k];
int zero = count[0];
int one = count[1];
if (j >= zero && k >= one){
dp[i][j][k] = Math.max(dp[i - 1][j][k],dp[i - 1][j - zero][k - one] + 1);
}
}
}
}
return dp[len][m][n];
}
//计算字符串中0和1的数量
private int[] calcuZeroAndOne(String str){
int[] res = new int[2];
for (char c : str.toCharArray()){
res[c - '0'] ++;
}
return res;
}
同样可以进行空间优化(三维数组转化为二维数组)
//2.动态规划空间优化(从后向前赋值)
public int findMaxForm(String[] strs, int m, int n) {
int[][] dp = new int[m + 1][n + 1];
dp[0][0] = 0;
for (String s : strs){
int[] count = calcuZeroAndOne(s);
int zero = count[0];
int one = count[1];
//或者将判断直接放到for循环中去
for (int i = m;i >= zero;i --){
for (int j = n;j >= one;j --){
dp[i][j] = Math.max(dp[i][j],dp[i - zero][j - one] + 1);
}
}
// for (int i = m;i >= 0;i --) {
// for (int j = n;j >= 0;j --) {
// if (i >= zero && j >= one){
// dp[i][j] = Math.max(dp[i][j],dp[i - zero][j - one] + 1);
// }
// }
// }
}
return dp[m][n];
}
//计算字符串中0和1的数量
private int[] calcuZeroAndOne(String str){
int[] res = new int[2];
for (char c : str.toCharArray()){
res[c - '0'] ++;
}
return res;
}
九.数组和矩阵
1.移动零(力扣283)
/*
* 很简单,从前往后遍历,后边补0
* */
public static void moveZeroes(int[] nums) {
int len = nums.length;
int index = 0;
for (int i = 0; i < len; i++) {
if (nums[i] != 0){
nums[index] = nums[i];
index ++;
}
}
for (int i = index; i < len; i++) {
nums[i] = 0;
}
}
更简洁
public void moveZeroes(int[] nums) {
int n = nums.length;
int index = 0;
for (int i = 0; i < n; i++) {
if (nums[i] != 0) {
nums[index++] = nums[i];
}
}
while (index < n) {
nums[index++] = 0;
}
}
2.重塑矩阵(力扣566)
//1.采用一维数组进行过渡
public static int[][] matrixReshape(int[][] mat, int r, int c) {
int m = mat.length;
int n = mat[0].length;
if (m * n != r * c){
return mat;
}
int[] arr = new int[m * n];
int index = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
arr[index ++] = mat[i][j];
}
}
int[][] ans = new int[r][c];
int index1 = 0;
for (int i = 0; i < r; i++) {
for (int j = 0; j < c; j++) {
ans[i][j] = arr[index1 ++];
}
}
return ans;
}
//2.映射
/*
* 对于二维数组元素(i,j)来说,他在把二维数组转化的一维数组中的位置为(i*n + j) = x
* 则有以下结果
* i = x / n;
* j = x % n;
* */
public static int[][] matrixReshape(int[][] mat, int r, int c) {
int m = mat.length;
int n = mat[0].length;
if (m * n != r * c){
return mat;
}
int[][] ans = new int[r][c];
for (int i = 0; i < m * n; i++) {
ans[i / c][i % c] = mat[i / n][i % n];
}
return ans;
}
3.最大连续 1 的个数(力扣485)
思路很简单
//1.遍历一次数组
public static int findMaxConsecutiveOnes(int[] nums) {
int max = 0,count = 0;
for (int i = 0; i < nums.length; i++) {
if (nums[i] != 0){
count ++;
}else{
count = 0;
}
max = Math.max(max,count);
}
return max;
}
4.搜索二维矩阵 II(力扣240)
//1.找规律
public static boolean searchMatrix(int[][] matrix, int target) {
int r = matrix.length, c = matrix[0].length;
int i = r - 1, j = 0;
while (i >= 0 && j < c) {
if (matrix[i][j] == target) {
return true;
} else if (matrix[i][j] < target) {
j++;
} else if (matrix[i][j] > target) {
i--;
}
}
return false;
}
//2.二分法搜索
private boolean binarySearch(int[][] matrix, int target, int start, boolean vertical) {
int left = start;
int right = vertical ? matrix.length - 1:matrix[0].length - 1;
while (left <= right){
int mid = left + (right - left) / 2;
//按列找
if (vertical){
if (matrix[start][mid] == target){
return true;
}else if (matrix[start][mid] < target){
left = mid + 1;
}else if (matrix[start][mid] > target){
right = mid - 1;
}
//按行找
}else{
if (matrix[mid][start] == target){
return true;
}else if (matrix[mid][start] < target){
left = mid + 1;
}else if (matrix[mid][start] > target){
right = mid - 1;
}
}
}
return false;
}
public boolean searchMatrix(int[][] matrix, int target) {
//当行或者列最短的那个结束之后,实际上已经遍历矩阵中所有元素了
int shorter = Math.min(matrix.length,matrix[0].length);
for (int i = 0; i < shorter; i++) {
boolean vFind = binarySearch(matrix,target,i,true);
boolean hFind = binarySearch(matrix,target,i,false);
if (vFind || hFind){
return true;
}
}
return false;
}
5.有序矩阵中第 K 小的元素(力扣378)
对于二分法具体细节还不明白,过一段时间再回头看。
//1.二分法
public static int kthSmallest(int[][] matrix, int k) {
int n = matrix.length;
int left = matrix[0][0];
int right = matrix[n - 1][n - 1];
while (left < right){
int mid = left + (right - left) / 2;
if (Search(matrix,mid,k,n)){
right = mid;
}else{
left = mid + 1;
}
}
return left;
}
public static boolean Search(int[][] matrix,int mid,int k,int n){
int i = n - 1,j = 0;
int sum = 0;
while (i >= 0 && j < n){
if (matrix[i][j] <= mid){
sum += (i + 1);
j ++;
}else{
i --;
}
}
return sum >= k;
}
6.错误的集合(力扣645)
思路很简单
先排序,遍历一遍找到重复的,同时求出错误的和,然后利用等差数列求和公式,求出正确的和,然后用错误的和减去重复的,就得到缺少的和,在用正确的和减去缺少的和,就得到缺少的数了
//1.先排序再搜索
public static int[] findErrorNums(int[] nums) {
int len = nums.length;
int[] res = new int[2];
Arrays.sort(nums);
for (int i = 1; i < len; i++) {
if (nums[i] == nums[i - 1]){
res[0] = nums[i];
}
}
int sum = 0;
for (int i : nums){
sum += i;
}
res[1] = res[0] + len * (len + 1) / 2 - sum;
return res;
}
//2.
/*
*看官方题解
* */
public static int[] findErrorNums(int[] nums) {
int dup = -1,missing = -1;
for (int i : nums){
if (nums[Math.abs(i) - 1] < 0){
dup = Math.abs(i);
}else{
nums[Math.abs(i) - 1] *= -1;
}
}
for (int i = 0; i < nums.length; i++) {
if (nums[i] > 0){
missing = i + 1;
}
}
return new int[]{dup,missing};
}
7.寻找重复数(力扣287)
//1.分桶思想
public int findDuplicate(int[] nums) {
int[] arr = new int[nums.length];
for(int i: nums){
arr[i] ++;
if(arr[i] > 1){
return i;
}
}
return -1;
}
//2.快慢指针
/*
其实,快慢指针法,就是一种 映射 操作, 链表 里面的 一次映射操作,
就是 求 next,且 将位置 更新到 这里;
数组 这里,就是 根据 下标 i 求 nums[i] 这个元素值,且 将 下标 更新到这里。
链表里面 有环,即 一个节点 被不同的 节点指向(映射);
而 这里说的 数组 有环,即 数组中的一个元素值 被不同的 index 指向(映射);
所以,求解方法 一样可以 使用 快慢指针法。
* */
public static int findDuplicate(int[] nums) {
int slow = 0,fast = 0;
do{
slow = nums[slow];
fast = nums[nums[fast]];
}while(slow != fast);
slow = 0;
while (slow != fast){
slow = nums[slow];
fast = nums[fast];
}
return slow;
}
二分法解释见官方
//3.二分法
/*
二分法是为了枚举可能重复的数:
用二分法在[1,2,3......,target,.......n]中选择一个候选值mid,
若mid处cnt<=mid,则说明target在mid右边:
[1,2,3......mid......target.......n] 反之,target在mid左边:
[1,2,3.......target.......mid.....n] 按上述判断结果重新划定[left,right],
得出新的候选值mid,继续判断
目的:找到最小的满足cnt[i]>i的i值;
* */
public static int findDuplicate(int[] nums) {
int len = nums.length;
int left = 1, right = len - 1,ans = -1;
while (left <= right) {
int mid = (left + right) >> 1;
int cnt = 0;
for (int i = 0; i < len; i++) {
if (nums[i] <= mid) {
cnt++;
}
}
if (cnt <= mid){
left = mid + 1;
//一旦cnt>mid,则mid即为所求目标target
}else{
right = mid - 1;
ans = mid;
}
}
return ans;
}
8.优美的排列 II(力扣667)
//1.纯找规律
public static int[] constructArray(int n, int k) {
int[] ans = new int[n];
int index = 1;
for (int i = 0; i <= k; i += 2) {
ans[i] = index++;
}
int index1 = k + 1;
for (int i = 1; i <= k; i += 2) {
ans[i] = index1--;
}
int index2 = k + 2;
for (int i = k + 1; i < n; i++) {
ans[i] = index2 ++;
}
return ans;
}
//2.官方题解
public static int[] constructArray(int n, int k) {
int[] ans = new int[n];
for (int i = 0; i < n - k - 1; i++) {
ans[i] = i + 1;
}
int left = n - k, right = n;
int j = 0;
for (int i = n - k - 1; i < n; i++) {
if (j % 2 == 0) {
ans[i] = left;
left++;
} else {
ans[i] = right;
right--;
}
j++;
}
return ans;
}
9.数组的度(力扣697)
思路很简单
要找出数组的众数,并且还有找出众数在数组中第一次出现和最后一次出现的位置,两个位置组成区间长度就是答案, 如果众数不止一个,那么要取区间长度最短那个
//1.哈希表
/*
* Map<Integer,int[]>
* Integet:数值;int[]:数组,[0]:个数 [1]:初始位置 [2]:结束位置
* */
public static int findShortestSubArray(int[] nums) {
int len = nums.length;
Map<Integer,int[]> map = new HashMap<Integer, int[]>();
for (int i = 0; i < len; i++) {
if (map.containsKey(nums[i])){
map.get(nums[i])[0] ++;
//记录结束位置
map.get(nums[i])[2] = i;
}else{
//记录个数,初始位置,结束位置
map.put(nums[i],new int[]{1,i,i});
}
}
int maxSum = 0,minLen = 0;
for (Map.Entry<Integer,int[]> entry:map.entrySet()){
int[] arr = entry.getValue();
if (maxSum < arr[0]){
maxSum = arr[0];
minLen = arr[2] - arr[1] + 1;
}else if (maxSum == arr[0]){
if (minLen > arr[2] - arr[1] + 1){
minLen = arr[2] - arr[1] + 1;
}
}
}
return minLen;
}
10.托普利茨矩阵(力扣766)
//1.很简单
public static boolean isToeplitzMatrix(int[][] matrix) {
int m = matrix.length,n = matrix[0].length;
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (matrix[i][j] != matrix[i - 1][j - 1]){
return false;
}
}
}
return true;
}
11.数组嵌套(力扣565)
//1.自己想的(时间空间都不好)
public static int arrayNesting(int[] nums) {
int len = nums.length;
Set<Integer> set = new HashSet<>();
int max = 0;
for (int i = 0; i < len; i++) {
int count = 1;
set.add(nums[i]);
int index = nums[i];
while (set.add(nums[index])){
index = nums[index];
count ++;
}
max = Math.max(max,count);
}
return max;
}
// 2.暴力法(常数空间)
public static int arrayNesting(int[] nums) {
int len = nums.length;
int max = 0;
for (int i = 0; i < len; i++) {
int start = nums[i],count = 0;
do {
start = nums[start];
count ++;
}while (start != nums[i]);
max = Math.max(max,count);
}
return max;
}
//3.使用额外空间
public static int arrayNesting(int[] nums) {
int len = nums.length;
boolean[] visited = new boolean[len];
int max = 0;
for (int i = 0; i < len; i++) {
int start = nums[i],count = 0;
if (!visited[i]){
do {
start = nums[start];
count ++;
visited[i] = true;
}while (start != nums[i]);
max = Math.max(max,count);
}
}
return max;
}
最优方法4
//4.使用原有数组
public static int arrayNesting(int[] nums) {
int len = nums.length;
int max = 0;
for (int i = 0; i < len; i++) {
if (nums[i] != Integer.MAX_VALUE) {
int start = nums[i], count = 0;
while (nums[start] != Integer.MAX_VALUE) {
int temp = start;
start = nums[start];
count++;
nums[temp] = Integer.MAX_VALUE;
}
max = Math.max(max, count);
}
}
return max;
}
12.最多能完成排序的块(力扣769)
思路很简单
//1.暴力解法
/*
当遍历到第i个位置时,如果可以切分为块,那前i个位置的最大值一定等于i。
否则,一定有比i小的数被划分到后面的块,那块排序后,一定不满足升序。
* */
public static int maxChunksToSorted(int[] arr) {
int count = 0,max = 0;
for (int i = 0; i < arr.length; i++) {
max = Math.max(max,arr[i]);
if (max == i) count ++;
}
return count;
}
十.数学
1.质数
1.计数质数(力扣204)
//1.暴力解法(超出时间限制)
public static int countPrimes(int n) {
int count = 0;
for (int i = 2; i < n; i++) {
if(isPrimes(i)){
count ++;
}
}
return count;
}
public static boolean isPrimes(int num){
//枚举[2,根号下num]即可
for (int i = 2; i * i <= num; i++) {
if (num % i == 0){
return false;
}
}
return true;
}
解释见官方
//2.埃氏筛
public static int countPrimes(int n) {
int[] isPrime = new int[n];
Arrays.fill(isPrime,1);
int count = 0;
for (int i = 2; i < n; i++) {
if (isPrime[i] == 1){
count ++;
//注意i * i的取值
if ((long)i * i < n){
for (int j = i * i; j < n; j+=i) {
isPrime[j] = 0;
}
}
}
}
return count;
}
2.进制转换
思路分析
先%再/
注意转换是否从0开始
1.七进制数(力扣504)
//2.先%再/
public static String convertToBase7(int num) {
StringBuilder sb = new StringBuilder();
if (num == 0) return "0";
boolean flag = num < 0;
num = Math.abs(num);
while (num != 0){
sb.append(num % 7);
num /= 7;
}
if (flag){
sb.append("-");
}
return sb.reverse().toString();
}
//3.利用函数
public static String convertToBase7(int num) {
return Integer.toString(num,7);
}
2.数字转换为十六进制数(力扣405)
//1.利用进制转换规律
/*
* 使用无符号右移 >>>
* */
public static String toHex(int num) {
StringBuilder sb = new StringBuilder();
char[] arr = "0123456789abcdef".toCharArray();
if (num ==0) return "0";
while (num != 0){
int temp = num & 15;
sb.append(arr[temp]);
//二进制->16进制,每四位二进制转换为一位16进制
num = num >>> 4;
}
return sb.reverse().toString();
}
3.Excel表列名称(力扣168)
public String convertToTitle(int columnNumber) {
StringBuilder sb = new StringBuilder();
while(columnNumber > 0){
int len = columnNumber % 26;
//特别注意这一步,因为转换不是从0开始的
if(len == 0){
len = 26;
columnNumber -=1;
}
sb.append((char)('A' + len - 1));
columnNumber /= 26;
}
return sb.reverse().toString();
}
//2.直接减1
public static String convertToTitle(int columnNumber) {
StringBuilder sb = new StringBuilder();
while (columnNumber > 0){
columnNumber --;
sb.append((char)('A' + columnNumber % 26));
columnNumber /= 26;
}
return sb.reverse().toString();
}
3.阶乘
思路很简单
//2.优化1
/*
* 找到规律,输入n,表示有n个数,每隔5个数出现一个5,
* 每隔25个数出现2个5,每隔125个数出现3个5...
* */
public static int trailingZeroes(int n) {
int count = 0;
while (n >= 5){
count += (n / 5);
n /= 5;
}
return count;
}
3.字符串加法减法
1.二进制求和(力扣67)
//1.先转换为十进制数,再转化为二进制数
public static String addBinary(String a, String b) {
return Integer.toBinaryString(
Integer.parseInt(a,2) + Integer.parseInt(b,2)
);
}
//2.位运算
public static String addBinary(String a, String b) {
int m = a.length() - 1, n = b.length() - 1;
StringBuilder sb = new StringBuilder();
int count = 0;
//循环相加两个字符串相同长度的低位数部分
while (m >= 0 && n >= 0) {
int sum = count;
sum += a.charAt(m--) - '0';
sum += b.charAt(n--) - '0';
//进位
count = sum / 2;
//当前位的值
sb.append(sum % 2);
}
// 如果 a 还没遍历完成(a串比b串长),则继续遍历添加 a 的剩余部分
while (m >= 0){
int sum = count + a.charAt(m--) - '0';
count = sum / 2;
sb.append(sum % 2);
}
// 如果 b 还没遍历完成(b串比a串长),则继续遍历添加 b 的剩余部分
while (n >= 0){
int sum = count + b.charAt(n--) - '0';
count = sum / 2;
sb.append(sum % 2);
}
//如果count不等于0 还有个进位数没加进去,需要补充
if (count == 1){
sb.append('1');
}
//反转字符串获得正常结果
return sb.reverse().toString();
}
2.字符串相加(力扣415)
//1.思路很简单(只不过是十进制)
public static String addStrings(String num1, String num2) {
StringBuilder sb = new StringBuilder();
int m = num1.length() - 1,n = num2.length() - 1;
int count = 0;
while (m >= 0 && n >= 0){
int sum = count;
sum += num1.charAt(m--) - '0';
sum += num2.charAt(n--) - '0';
count = sum / 10;
sb.append(sum % 10);
}
while (m >= 0) {
int sum = count + num1.charAt(m--) - '0';
count = sum / 10;
sb.append(sum % 10);
}
while (n >= 0) {
int sum = count + num2.charAt(n--) - '0';
count = sum / 10;
sb.append(sum % 10);
}
if (count == 1){
sb.append('1');
}
return sb.reverse().toString();
}
//2. 1的简化版
public static String addStrings(String num1, String num2) {
int m = num1.length() - 1,n = num2.length() - 1;
StringBuilder sb = new StringBuilder();
int count = 0;
while (m >= 0 || n >= 0 || count != 0){
//注意下面这一步
int sum1 = m >= 0 ? num1.charAt(m--) - '0': 0;
int sum2 = n >= 0 ? num2.charAt(n--) - '0': 0;
int sum = sum1 + sum2 + count;
count = sum / 10;
sb.append(sum % 10);
}
return sb.reverse().toString();
}
4.相遇问题
1.最少移动次数使数组元素相等 II(力扣462)
思路很简单
//1.把每个元素都变为中位数即可
public static int minMoves2(int[] nums) {
int len = nums.length;
Arrays.sort(nums);
int midNum = nums[len / 2];
int max = 0;
for (int i = 0; i < len; i++) {
max += Math.abs(nums[i] - midNum);
}
return max;
}
5.多数投票问题
1.多数元素(力扣169)
引用力扣评论中的一个形象描述
摩尔投票法:核心就是对拼消耗。
玩一个诸侯争霸的游戏,假设你方人口超过总人口一半以上,并且能保证每个人口出去干仗都能一对一同归于尽。最后还有人活下来的国家就是胜利。
那就大混战呗,最差所有人都联合起来对付你(对应你每次选择作为计数器的数都是众数),或者其他国家也会相互攻击(会选择其他数作为计数器的数),但是只要你们不要内斗,最后肯定你赢。
最后能剩下的必定是自己人。
//1.摩尔投票法
public static int majorityElement(int[] nums) {
int count = 1,maxNum = nums[0];
for (int i = 1; i < nums.length; i++) {
if (count == 0){
maxNum = nums[i];
}
if (nums[i] == maxNum){
count ++;
}else{
count --;
}
}
return maxNum;
}
//2.哈希表(单独写一个)
public static int majorityElement(int[] nums) {
//注意使用哈希表记录次数的写法
Map<Integer,Integer> map = new HashMap<>();
for (int i : nums){
if (!map.containsKey(i)){
map.put(i,1);
}
map.put(i,map.get(i) + 1);
}
int maxNum = 0,maxCount = 0;
//哈希表的遍历方法
for (Map.Entry<Integer,Integer> entry : map.entrySet()){
if (entry.getValue() > maxCount){
maxNum = entry.getKey();
maxCount = entry.getValue();
}
}
return maxNum;
}
6.其他
2.3的幂(力扣326)
//1.二分法
public static boolean isPowerOfThree(int n) {
long left = 0,right = n,mid,guessNum;
while (left <= right){
mid = (left + right) >> 1;
guessNum = (long) Math.pow(3, mid);
if (guessNum == n){
return true;
}else if (guessNum < n){
left = mid + 1;
}else{
right = mid - 1;
}
}
return false;
}
//2.找规律
public static boolean isPowerOfThree(int n) {
int count = 2;
while (n >= 3){
n -= count;
count *= 3;
}
return n == 1;
}
//3.迭代法
public static boolean isPowerOfThree(int n) {
if (n < 1){
return false;
}
//说明n是3的倍数
while (n % 3 == 0){
n /= 3;
}
return n == 1;
}
很巧妙
//4.找规律
public static boolean isPowerOfThree(int n) {
//int类型中最大的3的幂次为1162261467
return n > 0 && 1162261467 % n == 0;
}
3.除自身以外数组的乘积(力扣238)
说白了就是暴力解法,为了好理解第二种方法,所以写成分成左右两部分的形式
//1.超时(分为左右两部分,分别计算)
public static int[] productExceptSelf(int[] nums) {
int len = nums.length;
int[] res = new int[len];
for (int i = 0; i < len; i++) {
res[i] = 1;
}
for (int i = 0; i < len; i++) {
int left = i - 1;
while (left >= 0){
res[i] *= nums[left --];
}
int right = i + 1;
while(right < len){
res[i] *= nums[right ++];
}
}
return res;
}
//2.(分为左右两部分,分别计算)降低时间复杂度
public static int[] productExceptSelf(int[] nums) {
int len = nums.length;
int[] res = new int[len];
//res[i]表示i左边的数的乘积
res[0] = 1;
for (int i = 1; i < len; i++) {
res[i] = nums[i - 1] * res[i - 1];
}
//乘完左边乘右边
int right = 1;
for (int i = len - 1; i >= 0 ; i--) {
res[i] = res[i] * right;
right *= nums[i];
}
return res;
}
4.三个数的最大乘积(力扣628)
/*
1.如果数组中全是非负数,则排序后最大的三个数相乘即为最大乘积;
2.如果全是非正数,则最大的三个数相乘同样也为最大乘积。
3.如果数组中有正数有负数,则最大乘积既可能是三个最大正数的乘积,
也可能是两个最小负数(即绝对值最大)与最大正数的乘积。
*/
//1.先排序
public static int maximumProduct(int[] nums) {
Arrays.sort(nums);
int len = nums.length;
return Math.max(nums[len - 1] * nums[len - 2] * nums[len - 3], nums[0] * nums[1] * nums[len - 1]);
}
//2.线性扫描
/*
在方法一中,我们实际上只要求出数组中最大的三个数以及最小的两个数,
因此我们可以不用排序,用线性扫描直接得出这五个数。
*/
public static int maximumProduct(int[] nums) {
//min1:最小;min2:第二小
int min1 = Integer.MAX_VALUE,min2 = Integer.MAX_VALUE;
int max1 = Integer.MIN_VALUE,max2 = Integer.MIN_VALUE,max3 = Integer.MIN_VALUE;
for (int i : nums){
if (i < min1){
min2 = min1;
min1 = i;
}else if (i < min2){
min2 = i;
}
if (i > max1){
max3 = max2;
max2 = max1;
max1 = i;
}else if (i > max2){
max3 = max2;
max2 = i;
}else if (i > max3){
max3 = i;
}
}
return Math.max(max1*max2*max3,min1*min2*max1);
}
十一.字符串
1.有效的字母异位词(力扣242)
//1.先排序再比较
public static boolean isAnagram(String s, String t) {
if (s.length() != t.length()){
return false;
}
char[] arr1 = s.toCharArray();
char[] arr2 = t.toCharArray();
Arrays.sort(arr1);
Arrays.sort(arr2);
return Arrays.equals(arr1,arr2);
}
//2.哈希表
/*
getOrdefault():如果包含此key值,返回对应的value;
如果不包含,返回默认值。
*/
public static boolean isAnagram(String s, String t) {
if (s.length() != t.length()){
return false;
}
Map<Character,Integer> map = new HashMap<>();
for (char c1 : s.toCharArray()){
map.put(c1,map.getOrDefault(c1,0) + 1);
}
for (char c2 : t.toCharArray()){
map.put(c2,map.getOrDefault(c2,0) - 1);
if (map.get(c2) < 0){
return false;
}
}
return true;
}
//3.2的改进
/*
可以假设字符串只包含小写字母,用数组代替哈希表
*/
public static boolean isAnagram(String s, String t) {
if (s.length() != t.length()){
return false;
}
int[] arr = new int[26];
for (char c1 : s.toCharArray()){
int i = c1 - 'a';
arr[i] ++;
}
for (char c2 : t.toCharArray()){
int j = c2 - 'a';
arr[j] --;
if (arr[j] < 0){
return false;
}
}
return true;
}
2.最长回文串(力扣409)
刚开始自己想到的方法
//1.哈希表
public static int longestPalindrome(String s) {
Map<Character,Integer> map = new HashMap<>();
for (char c : s.toCharArray()){
map.put(c,map.getOrDefault(c,0) + 1);
}
int maxLen = 0;
boolean flag = false;
for (Map.Entry<Character,Integer> entry : map.entrySet()){
if (entry.getValue() > 0 && entry.getValue() % 2 == 0){
maxLen += entry.getValue();
}
if (entry.getValue() >= 3 && entry.getValue() % 2 != 0){
maxLen += (entry.getValue() - 1);
}
if (!flag){
if (entry.getValue() % 2 != 0){
flag = true;
}
}
}
return flag ? maxLen + 1 : maxLen;
}
//2.找规律
public static int longestPalindrome(String s) {
int[] arr = new int[128];
for (char c : s.toCharArray()){
//这里用的是类型转换,char自动转换为int
arr[c] ++;
}
int count = 0;
for (int i : arr){
//记录次数为 奇数 的个数
count += (i % 2);
}
//如果全为偶数,自然是全长;如果存在奇数,减去奇数的个数再加1
return count == 0 ? s.length() : s.length() - count + 1;
}
3.同构字符串(力扣205)
//1.哈希表(映射关系必须为1对1)
public static boolean isIsomorphic(String s, String t) {
Map<Character,Character> s2t = new HashMap<>();
Map<Character,Character> t2s = new HashMap<>();
int len = s.length();
for (int i = 0; i < len; i++) {
char x = s.charAt(i);
char y = t.charAt(i);
if (s2t.containsKey(x) && s2t.get(x) != y || t2s.containsKey(y) && t2s.get(y) != x){
return false;
}
s2t.put(x,y);
t2s.put(y,x);
}
return true;
}
4.回文子串(力扣647)
解释见官方
//1.中心拓展法
public static int countSubstrings(String s) {
int len = s.length();
int ans = 0;
for (int i = 0; i < len * 2 - 1; i++) {
int left = i / 2,right = left + i % 2;
while(left >= 0 && right < len && s.charAt(left) == s.charAt(right)){
left --;
right ++;
ans ++;
}
}
return ans;
}
5.回文数(力扣9)
//1.反转一半
public static boolean isPalindrome(int x) {
if (x < 0 || (x != 0 && x % 10 == 0)){
return false;
}
int reverseNum = 0;
while(x > reverseNum){
//reverseNum在不断增大
reverseNum = reverseNum * 10 + (x % 10);
//x在不断减小
x /= 10;
}
//当x == reverseNum 时长度为偶数
//当x == reverseNum / 10时长度为奇数
return x == reverseNum || x == reverseNum / 10;
}
//2.如果可以用字符串
public static boolean isPalindrome(int x) {
String reverseStr = new StringBuilder(x + "").reverse().toString();
return (x + "").equals(reverseStr);
}
6.最长回文子串(力扣5)
//1.动态规划
public static String longestPalindrome(String s) {
int len = s.length();
// 特判
if (len < 2){
return s;
}
int maxLen = 1;
int begin = 0;
// 1. 状态定义
// dp[i][j] 表示s[i...j] 是否是回文串
// 2. 初始化
boolean[][] dp = new boolean[len][len];
for (int i = 0; i < len; i++) {
dp[i][i] = true;
}
char[] chars = s.toCharArray();
// 3. 状态转移
// 注意:先填左下角
// 填表规则:先一列一列的填写,再一行一行的填,
// 保证左下方的单元格先进行计算
for (int j = 1;j < len;j++){
for (int i = 0; i < j; i++) {
// 头尾字符不相等,不是回文串
if (chars[i] != chars[j]){
dp[i][j] = false;
}else {
// 相等的情况下
// 考虑头尾去掉以后没有字符剩余(j - i) = 1,
// 或者剩下一个字符的时候(j - i) = 2,肯定是回文串
if (j - i < 3){
dp[i][j] = true;
}else {
// 状态转移
//保证 (i + 1) < (j - 1) -> j - i > 2
dp[i][j] = dp[i + 1][j - 1];
}
}
// 只要dp[i][j] == true 成立,表示s[i...j] 是否是回文串
// 此时更新记录回文长度和起始位置
if (dp[i][j] && j - i + 1 > maxLen){
maxLen = j - i + 1;
begin = i;
}
}
}
// 4. 返回值
return s.substring(begin,begin + maxLen);
}
//2.中心扩展法
public static String longestPalindrome(String s) {
// 初始化最大回文子串的起点和终点
int start = 0;
int end = 0;
// 遍历每个位置,当做中心位
for (int i = 0; i < s.length(); i++) {
// 分别拿到奇数偶数的回文子串长度
int len_odd = expandCenter(s,i,i);
int len_even = expandCenter(s,i,i + 1);
// 对比最大的长度
int len = Math.max(len_odd,len_even);
// 计算对应最大回文子串的起点和终点
if (len > end - start){
start = i - (len - 1)/2;
end = i + len/2;
}
}
// 注意:这里的end+1是因为 java自带的左闭右开的原因
return s.substring(start,end + 1);
}
/**
* @param s 输入的字符串
* @param left 起始的左边界
* @param right 起始的右边界
* @return 回文串的长度
*/
public static int expandCenter(String s,int left,int right){
// left = right 的时候,此时回文中心是一个字符,回文串的长度是奇数
// right = left + 1 的时候,此时回文中心是一个空隙,回文串的长度是偶数
// 跳出循环的时候恰好满足 s.charAt(left) != s.charAt(right)
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)){
left--;
right++;
}
// 回文串的长度是right-left+1-2 = right - left - 1
return right - left - 1;
}
//2.中心扩展法(奇数偶数情况合起来)
public static String longestPalindrome(String s) {
int len = s.length();
int start = 0,maxLen = 1;
for (int i = 0; i < 2 * len - 1; i++) {
int left = i / 2,right = left + i % 2;
while (left >= 0 && right < len && s.charAt(left) == s.charAt(right)){
left --;
right ++;
}
if (right - left - 1 > maxLen){
maxLen = right - left - 1;
start = left + 1;
}
}
return s.substring(start,start + maxLen);
}
7.计数二进制子串(力扣696)
//1.利用额外空间
public static int countBinarySubstrings(String s) {
ArrayList<Integer> list = new ArrayList<>();
int len = s.length();
int pre = 0;
while (pre < len){
char c = s.charAt(pre);
int count = 0;
while (pre < len && c == s.charAt(pre)){
pre ++;
count ++;
}
list.add(count);
}
int max = 0;
for (int i = 1; i < list.size(); i++) {
max += Math.min(list.get(i),list.get(i - 1));
}
return max;
}
//2.利用常数空间
public static int countBinarySubstrings(String s) {
int len = s.length();
int pre = 0;
int first = 0,max = 0;
while (pre < len){
char c = s.charAt(pre);
int count = 0;
while (pre < len && c == s.charAt(pre)){
pre ++;
count ++;
}
max += Math.min(first,count);
first = count;
}
return max;
}
十二.链表
1.相交链表(力扣160)
/*
* 题解:
* 设链表A的长度为a+c,链表B的长度为b+c。
* a为链表A不公共部分,b为链表B不公共部分,c为链表A、B的公共部分
* 将两个链表连起来,A->B和B->A,长度:a+c+b+c=b+c+a+c.
*
* 1.若链表AB相交,则a+c+b与b+c+a就会抵消,
* 2.若不相交,则a+b=b+a,它们各自移动到尾部循环结束
* (此时node1 = node2 = null,不满足循环条件),即返回null
* 3.如果链表AB长度相同且相交,会在交点处相等,退出循环
* */
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null){
return null;
}
ListNode node1 = headA,node2 = headB;
while (node1 != node2){
node1 = node1 == null ? headB:node1.next;
node2 = node2 == null ? headA:node2.next;
}
return node1;
}
2.反转链表(力扣206)
//1.迭代(三个节点)
public ListNode reverseList(ListNode head) {
//由于节点没有引用其上一个节点,因此必须事先存储其前一个元素
ListNode preNode = null;
ListNode curNode = head;
while (curNode != null){
//在更改引用之前,还需要另一个指针来存储下一个节点
ListNode nextNode = curNode.next;
//在遍历列表时,将当前节点的next指针改为指向前一个元素
curNode.next = preNode;
preNode = curNode;
curNode = nextNode;
}
//不要忘记在最后返回新的头引用!
//此时curNode == null,preNode指向最后一个元素
return preNode;
}
//2.递归
/*
* 总结:
* 我子节点下的所有节点都已经反转好了,
* 现在就剩我和我的子节点 没有完成最后的反转了,所以反转一下我和我的子节点。
*
* 思路:
* 不妨假设链表为1,2,3,4,5。
* 按照递归,当执行reverseList(5)的时候返回了5这个节点,
* reverseList(4)中的p就是5这个节点,我们看看reverseList(4)接下来执行完之后,
* 5->next = 4, 4->next = null。这时候返回了p这个节点,也就是链表5->4->null,
* 接下来执行reverseList(3),代码解析为4->next = 3,3->next = null,
* 这个时候p就变成了,5->4->3->null, reverseList(2), reverseList(1)依次类推,
* p就是:5->4->3->2->1->null
* */
//空间复杂度:O(n),由于使用递归,将会使用隐式栈空间。
//递归深度可能会达到n层。
public ListNode reverseList(ListNode head) {
if (head == null || head.next == null){
return head;
}
ListNode p = reverseList(head.next);
head.next.next = head;
head.next = null;
return p;
}
3.合并两个有序链表(力扣21)
//1.递归
/*
* 递归时不要想递归进去是什么,而是想递归获得了什么结果,
* 然后关注递归后对递归结果的操作就行,这样不用费脑子就能写出递归
* */
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if (l1 == null){
return l2;
}else if (l2 == null){
return l1;
}else if (l1.val < l2.val){
l1.next = mergeTwoLists(l1.next,l2);
return l1;
}else{
l2.next = mergeTwoLists(l1,l2.next);
return l2;
}
}
//2.迭代
/*
node:排头兵,保持在最前面不动,方便返回链表
preNode: 不停移动,串接l1和l2
*/
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode node = new ListNode(-1);
ListNode preNode = node;
while (l1 != null && l2 != null){
if (l1.val <= l2.val){
preNode.next = l1;
l1 = l1.next;
}else{
preNode.next = l2;
l2 = l2.next;
}
preNode = preNode.next;
}
//合并后 l1 和 l2 最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可
preNode.next = l1 == null ? l2 : l1;
return node.next;
}
4.删除排序链表中的重复元素(力扣83)
//1.自己想的
public ListNode deleteDuplicates(ListNode head) {
if (head == null || head.next == null){
return head;
}
ListNode preNode = head;
ListNode curNode = head.next;
while (curNode != null){
if (curNode.val == preNode.val){
preNode.next = curNode.next;
curNode = preNode.next;
}else{
preNode = curNode;
curNode = curNode.next;
}
}
return head;
}
//2.一次遍历
public ListNode deleteDuplicates(ListNode head) {
if (head == null){
return head;
}
ListNode curNode = head;
while (curNode.next != null){
if (curNode.val == curNode.next.val){
curNode.next = curNode.next.next;
}else{
curNode = curNode.next;
}
}
return head;
}
5.删除链表的倒数第 N 个结点(力扣19)
//1.自己想的
public ListNode removeNthFromEnd(ListNode head, int n) {
int count = 1;
ListNode first = head;
while (first.next != null){
first = first.next;
count ++;
}
if (count == n){
return head.next;
}
int index = 1;
ListNode cur = head;
while (cur.next != null){
if (index == count - n){
cur.next = cur.next.next;
break;
}
cur = cur.next;
index ++;
}
return head;
}
//2.双指针
/*
* 初始时,first -> head,second -> node(便于节点删除),让first先走n步,这时first和second之间
* 相差n+1步,再让first和second一起走,当first=null时,second到达被删除节点的前
* 一个节点。
* */
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode node = new ListNode(0,head);
ListNode first = head,second = node;
for (int i = 0; i < n; i++) {
first = first.next;
}
while (first != null){
first = first.next;
second = second.next;
}
second.next = second.next.next;
return node.next;
}
6.两两交换链表中的节点(力扣24)
//1.递归
/*
* 1.找终止条件
* 2.找返回值
* 3.具体操作步骤
*
*先排后面,再依次往前排
*举例:
* 1 -> 2 -> 3 -> 4
* head node
*
*第一次: 3 -> null
* 4 -> 3 -> null(整个链表作为返回值)
*第二次: 1 -> 4 -> 3 -> null
* 2 -> 1 -> 4 -> 3 -> null
*
*这个过程无论总数是奇数还是偶数都一样,无非是返回null还是最后一个节点的问题
* */
public ListNode swapPairs(ListNode head) {
if (head == null || head.next == null){
return head;
}
ListNode node = head.next;
head.next = swapPairs(node.next);
node.next = head;
return node;
}
//2.迭代
//自己手动写写就明白了(举例 1 -> 2-> 3-> 4)
public ListNode swapPairs(ListNode head) {
//排头兵固定不动,方便返回结果
ListNode node = new ListNode(0);
//指向反转后的第二个元素,保留前面的信息
ListNode curNode = node;
curNode.next = head;
while (curNode.next != null && curNode.next.next != null){
ListNode node1 = curNode.next;
ListNode node2 = curNode.next.next;
//主要反转过程
curNode.next = node2;
node1.next = node2.next;
node2.next = node1;
//移动curNode,进行下两个元素的反转
curNode = node1;
}
return node.next;
}
7.两数相加 II(力扣445)
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
Deque<Integer> deque1 = new LinkedList<>();
Deque<Integer> deque2 = new LinkedList<>();
while (l1 != null){
deque1.push(l1.val);
l1 = l1.next;
}
while (l2 != null) {
deque2.push(l2.val);
l2 = l2.next;
}
int count = 0;
ListNode head = null;
while (!deque1.isEmpty() || !deque2.isEmpty() || count != 0) {
int a = deque1.isEmpty() ? 0 : deque1.pop();
int b = deque2.isEmpty() ? 0 : deque2.pop();
int cur = a + b + count;
count = cur / 10;
cur %= 10;
ListNode curNode = new ListNode(cur);
curNode.next = head;
head = curNode;
}
return head;
}
8.回文链表(力扣234)
//1.自己想的(栈)
public boolean isPalindrome(ListNode head) {
Deque<Integer> deque = new LinkedList<>();
ListNode node = head;
while (node != null){
deque.push(node.val);
node = node.next;
}
while (head != null){
if (head.val == deque.peek()){
deque.pop();
head = head.next;
}else{
return false;
}
}
return true;
}
//2.数组列表+双指针
public boolean isPalindrome(ListNode head) {
List<Integer> list = new ArrayList<>();
ListNode node = head;
while (node != null){
list.add(node.val);
node = node.next;
}
int front = 0;
int last = list.size() - 1;
while (front < last){
if (list.get(front) != list.get(last)){
return false;
}
front ++;
last --;
}
return true;
}
//3.递归(从后往前分析)
private ListNode pointerNode;
public boolean isPalindrome(ListNode head) {
pointerNode = head;
return recursion(head);
}
public boolean recursion(ListNode curNode){
if (curNode != null){
if (!recursion(curNode.next)){
return false;
}
if (curNode.val != pointerNode.val){
return false;
}
pointerNode = pointerNode.next;
}
return true;
}
//4.快慢指针
public boolean isPalindrome(ListNode head) {
ListNode endOfFirstHalfNode = endOfFirstHalf(head);
ListNode startOfSecondNode = reverseList(endOfFirstHalfNode.next);
while (startOfSecondNode != null){
if (startOfSecondNode.val != head.val){
return false;
}
startOfSecondNode = startOfSecondNode.next;
head = head.next;
}
return true;
}
//旋转链表
private ListNode reverseList(ListNode head){
ListNode pre = null;
ListNode cur = head;
while (cur != null){
ListNode temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
}
return pre;
}
//利用快慢指针找到前半部分链表的最后一个节点
private ListNode endOfFirstHalf(ListNode head){
ListNode slowNode = head;
ListNode fastNode = head;
while (fastNode.next != null && fastNode.next.next != null){
slowNode = slowNode.next;
fastNode = fastNode.next.next;
}
return slowNode;
}
9.分隔链表(力扣725)
//1.思路想到了,代码没写出来
public ListNode[] splitListToParts(ListNode head, int k) {
int len = 0;
ListNode first = head;
while (first != null){
len ++;
first = first.next;
}
int size = len / k;
int mod = len % k;
//无论len < k 还是len > k,数组的大小都是k
ListNode[] res = new ListNode[k];
first = head;
for (int i = 0; i < k && first != null; i++) {
res[i] = first;
//需要向后面移动多少步
int curSize = size + (mod-- > 0 ? 1 : 0);
//关键一步,当时没想出来怎样实现向后面走多少步
for (int j = 0; j < curSize - 1; j++) {
first = first.next;
}
//断开前面节点和后面的关系
ListNode node = first.next;
first.next = null;
first = node;
}
return res;
}
十三.排序
1.基本排序算法总结
1.1 冒泡排序
//1.冒泡排序
/*
* 算法分析
最佳情况:T(n) = O(n)
最差情况:T(n) = O(n2)
平均情况:T(n) = O(n2)
两两依次比较,将大值后移,每一趟将最大值放到最后
* */
public static int[] bubbleSort(int[] array){
int len = array.length;
boolean flag = false;
for (int i = 0; i < len - 1; i++) {
for (int j = 0; j < len - 1 - i; j++) {
if (array[j + 1] < array[j]){
flag = true;
int temp = array[j + 1];
array[j + 1] = array[j];
array[j] = temp;
}
}
if (!flag){
break;
}else{
flag = false;
}
}
return array;
}
1.2 选择排序
//2.选择排序
/*
* 算法分析
最佳情况:T(n) = O(n2)
最差情况:T(n) = O(n2)
平均情况:T(n) = O(n2)
假定第1个元素是最小值,然后依次与后面的元素相比较,如果在后面元素中发现比它小的元素,
则交换,一轮完成之后最小的元素放到第1个位置。然后再假设第2个元素是最小元素,依次类推...
* */
public static int[] selectionSort(int[] array){
int len = array.length;
for (int i = 0; i < len - 1; i++) {
//预定的最小下标
int minIndex = i;
//预定的最小数
int min = array[i];
for (int j = i + 1; j < len; j++) {
if (array[j] < min){
minIndex = j;
min = array[j];
}
}
if (minIndex != i){
array[minIndex] = array[i];
array[i] = min;
}
}
return array;
}
1.3 插入排序
//3.插入排序
/*
* 算法分析
最佳情况:T(n) = O(n)
最坏情况:T(n) = O(n2)
平均情况:T(n) = O(n2)
假定前N个元素为有序的,则拿出第N+1个元素与前面元素相比,
放到适当的位置,然后前N+1个元素是有序的,再取第N+2个元素
与前面的相比,依次类推...
* */
public static int[] insertionSort(int[] array){
int len = array.length;
//待插入的数
int insertVal = 0;
//待插入的下标
int insertIndex = 0;
for (int i = 1; i < len; i++) {
insertVal = array[i];
insertIndex = i - 1;
while (insertIndex >= 0 && insertVal < array[insertIndex]){
array[insertIndex + 1] = array[insertIndex];
insertIndex --;
}
if (insertIndex + 1 != i){
array[insertIndex + 1] = insertVal;
}
}
return array;
}
1.4 希尔排序
//1.插入时采用交换法
public static int[] shellSort(int[] array){
//交换时的中间变量
int temp = 0;
int len = array.length;
for (int gap = len / 2;gap > 0;gap /= 2) {
for (int i = gap; i < len; i++) {
for (int j = i - gap; j >= 0; j -= gap) {
if (array[j] > array[i]){
temp = array[j];
array[j] = array[j + gap];
array[j + gap] = temp;
}
}
}
}
return array;
}
//2.对交换式的希尔排序进行优化->移位式
public static int[] shellSort(int[] array) {
int len = array.length;
for (int gap = len / 2; gap > 0; gap /= 2) {
for (int i = gap; i < len; i++) {
int index = i;
int temp = array[i];
if (array[index] < array[index - gap]) {
while (index - gap>= 0 && temp < array[index - gap]){
array[index] = array[index - gap];
index -= gap;
}
array[index ] = temp;
}
}
}
return array;
}
1.5 归并排序(递归)
public static void mergeSort(int[] arr, int left, int right, int[] temp) {
if (left < right) {
int mid = (left + right) / 2;
//向左递归进行分解
mergeSort(arr, left, mid, temp);
//向右递归进行分解
mergeSort(arr, mid + 1, right, temp);
//合并
merge(arr, left, mid, right, temp);
}
}
//合并两个有序数组
public static void merge(int[] arr, int left, int mid, int right, int[] temp) {
//初始化i,左边有序序列的初始索引
int i = left;
//初始化j,右边有序序列的初始索引
int j = mid + 1;
//指向temp数组的当前索引
int t = 0;
//1.第一种情况
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
temp[t] = arr[i];
i++;
t++;
} else {
temp[t] = arr[j];
j ++;
t ++;
}
}
//2.第二种情况
while (i <= mid){
temp[t] = arr[i];
i++;
t++;
}
while (j <= right){
temp[t] = arr[j];
j ++;
t ++;
}
//3.将temp复制到arr
t = 0;
int tempLeft = left;
while (tempLeft <= right){
arr[tempLeft] = temp[t];
t ++;
tempLeft ++;
}
}
1.6 快速排序(对冒泡排序的一种改进)
public static void quickSort(int[] arr,int left,int right){
int l = left;//左下标
int r = right;//右下标
int pivot = arr[(left + right) / 2];//中轴值
int temp = 0;//临时变量,交换时使用
//while循环的目的:比pivot小的值放到左边,比他大的值放到右边
while (l < r){
//从左边开始一直找,直到找到一个比pivot大的值
while (arr[l] < pivot){
l ++;
}
//从右边开始一直找,直到找到一个比pivot小的值
while (arr[r] > pivot){
r --;
}
//如果l >= r,说明此时pivot左边的值都比它小,右边的值都比它大
if (l >= r){
break;
}
//交换
temp = arr[l];
arr[l] = arr[r];
arr[r] = temp;
//这里是优化
//如果已经交换的两个数都等于pivot,会出现死循环
//arr[l] == pivot已经到达左边能够到达的最大限度,所以r --
if (arr[l] == pivot){
r --;
}
if (arr[r] == pivot){
l ++;
}
}
if (l == r){
l ++;
r --;
}
if (left < r){
quickSort(arr,left,r);
}
if (right > l){
quickSort(arr,l,right);
}
}
1.7 堆排序(大根堆,小根堆)
public static void heapSort(int[] array){
// 按照完全二叉树的特点,从最后一个非叶子节点开始,对于整棵树进行大根堆的调整
// 也就是说,是按照自下而上,每一层都是自右向左来进行调整的
// 注意,这里元素的索引是从0开始的
//i = array.length / 2 - 1 -> 找到第一个非叶子节点
//第一个非叶子结点 arr.length/2-1=5/2-1=1
for (int i = array.length / 2 - 1; i >= 0; i--) {
adjustHeap(array, i, array.length);
}
// 上述逻辑,建堆结束
// 下面,开始排序逻辑
for (int j = array.length - 1; j > 0; j--) {
// 元素交换
// 说是交换,其实质就是把大顶堆的根元素(最大元素),放到数组的最后;换句话说,
// 就是每一次的堆调整之后,都会有一个元素到达自己的最终位置
swap(array, 0, j);
// 元素交换之后,毫无疑问,最后一个元素无需再考虑排序问题了。
// 接下来我们需要排序的,就是已经去掉了部分元素的堆了,这也是为什么此方法放在循环里的原因
// 而这里,实质上是自上而下,自左向右进行调整的
adjustHeap(array, 0, j);
}
}
/**
* @description 这里,是整个堆排序最关键的地方
* i:非叶子节点
*/
public static void adjustHeap(int[] array, int i, int length) {
// 先把当前元素取出来,因为当前元素可能要一直移动
int temp = array[i];
// 接下来的讲解,都是按照i的初始值为0来讲述的(最终i会等于0)
// 这一段很好理解,如果i=0;则k=1;k+1=2
// 实质上,就是根节点和其左右子节点进行比较,让k指向这个不超过三个节点的子树中最大的值
// 这里,必须要说下为什么k值是跳跃性的。
// 首先,举个例子,如果a[0] > a[1] && a[0]>a[2],说明0,1,2这棵树不需要调整,
// 那么,下一步该到哪个节点了呢?肯定是a[1]所在的子树了
// 也就是说,是以本节点的左子节点为根的那棵小的子树
// 而如果a[0]<a[2]呢,那就调整a[0]和a[2]的位置,然后继续调整以a[2]为根节点的那棵子树,
// 而且肯定是从左子树开始调整的
// 所以,这里面的用意就在于,自上而下,自左向右一点点调整整棵树的部分,
// 直到每一颗小子树都满足大根堆的规律为止
for (int k = 2 * i + 1; k < length; k = 2 * k + 1) {
// 让k先指向子节点中最大的节点
if (k + 1 < length && array[k] < array[k + 1]) {
k++;
}
// 如果发现子节点更大,则进行值的交换
if (array[k] > temp) {
swap(array, i, k);
// 下面就是非常关键的一步了
// 如果子节点更换了,那么,以子节点为根的子树会不会受到影响呢?
// 所以,循环对子节点所在的树继续进行判断
i = k;
// 如果不用交换,那么,就直接终止循环了
} else {
break;
}
}
}
/**
* 交换元素
* @param arr
* @param a
* 元素的下标
* @param b
* 元素的下标
*/
public static void swap(int[] arr, int a, int b) {
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
1.8 基数排序(空间换时间)
//8.基数排序(空间换时间)
/*
基数排序也是非比较的排序算法,对每一位进行排序,从最低位开始排序,复杂度为O(kn),
n为数组长度,k为数组中的数的最大的位数;
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;
依次类推,直到最高位。有时候有些属性是有优先级顺序的,
步骤1:取得数组中的最大数,并取得位数;
步骤2:排序
步骤3:收集
* */
public static void baseSort(int[] arr){
//1.得到数组中最大数的位数
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max){
max = arr[i];
}
}
//最大数的长度
int maxLength = (max + "").length();
//用二维数组表示一个桶,桶的数量为10个,每个桶的容量是arr.length
int[][] bucket = new int[10][arr.length];
//为了记录每个桶中实际存放了多少个数据,定义一个一维数组来记录每个桶每次放入的数据个数
int[] bucketElementCounts = new int[10];
for (int i = 0,n = 1; i < maxLength; i++,n *= 10) {
//2.放入
for (int j = 0; j < arr.length; j++) {
int digitalElement = arr[j] / n % 10;
bucket[digitalElement][bucketElementCounts[digitalElement]] = arr[j];
bucketElementCounts[digitalElement] ++;
}
//3.依次将桶中元素取出,放到arr中,顺序是按照放入时那一位的顺序
int index = 0;
for (int k = 0; k < bucketElementCounts.length; k++) {
if (bucketElementCounts[k] != 0){
for (int l = 0; l < bucketElementCounts[k]; l++) {
arr[index ++] = bucket[k][l];
}
}
//!!!!!!!!!
bucketElementCounts[k] = 0;
}
}
}
十四.算法
1.二分算法
1.1 模板
2.分治算法
3.动态规划算法
4.KMP算法
这是KMP算法的博客,还没有看,等哪天对算法有了更深的认识,再回过头来看
//1.暴力匹配
public static int violenceMatch(String s1,String s2){
char[] c1 = s1.toCharArray();
char[] c2 = s2.toCharArray();
int s1Length = s1.length();
int s2Length = s2.length();
int i = 0,j = 0;
while (i < s1Length && j < s2Length){
if (c1[i] == c2[j]){
i ++;
j ++;
}else{
//回到上一次开始比较的下一个位置
i = i - j + 1;
j = 0;
}
if (j == s2Length - 1){
//返回第一次出现的位置
return i - j;
}
}
return -1;
}
推荐视频KMP
//KMP算法
public static int Kmp(String s1,String s2,int[] next){
for (int i = 0,j = 0; i < s1.length(); i++) {
/*
* s1:文本串 s2:模式串
* a a b a a b a a f
* i
* 0 1 2 3 4 5
* a a b a a f
* j
*
*此时b != f , j 回退到 j == 2,因为知道文本串中有aa和模式串中aa相等,
*此时b != f , j 回退到 j == 2,因为知道文本串中有aa和模式串中aa相等,
*而模式串自己0和1位置的aa和3,4位置的aa相等,所以aa不用再做比较。
*如果j==2时仍然不相等,接着回退,以此类推...
*所以用while
* */
while (j > 0 && s1.charAt(i) != s2.charAt(j)){
j = next[j - 1];
}
if (s1.charAt(i) == s2.charAt(j)) {
j ++;
}
if (j == s2.length()){
return i - j + 1;
}
}
return -1;
}
//获取一个字符串的部分匹配值表
public static int[] kmpNext(String s){
//1.初始化
int[] next = new int[s.length()];
next[0] = 0;
for (int i = 1,j = 0; i < s.length(); i++) {
//2.前后缀不相同
//防止数组下标越界,因为不停回退,使用while而不是if
/*
* a a b a a f
* j i
* 0 1 0 1 2
*
* 前缀:必须包含第一个元素
* 后缀:必须包含最后一个元素
*
* 此时不相等,说明此时的前缀aab 和 后缀 aaf 不匹配,长度为3的前缀和后缀不匹配
* 接下来比较长度为2的前缀 aa 和后缀 af 是否匹配,依然不匹配,依次类推...
* 这也是使用while循环的用意(所比较的前缀和后缀的长度依次缩短)
*
* 如果将aabaaf改为afbaaf,则第一次回退的过程中发现有相等的情况出现...
* */
while (j > 0 && s.charAt(i) != s.charAt(j)){
j = next[j - 1];
}
//3.前后缀相同
if (s.charAt(i) == s.charAt(j)){
j ++;
}
//4.填充next数组
next[i] = j;
}
return next;
}