常用算法
最长递增子序列长度
给你一个整数数组 nums,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
解题思路:找出序列中最长子序列的长度
1.动态规划法
动态规划,从前一状态转移到后一状态
为了形成更长的上升子序列,在以往子序列中寻找最大的值
回文链表
给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。
回文链表:从字符串正反两边读起,得到的字符内容都是相等的。
思路1:暴力枚举法(空间复杂度O(n))
取出单链表中的所有的数,装在List中,再二分法判断相等
思路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 boolean isPalindrome(ListNode head) {
String a1="";
String a2="";
if (head == null){
return false;
}
while(head != null){
a1 = a1 + head.val;
a2 = head.val + a2;
head = head.next;
}
if(a1.equals(a2)){
return true;
}
else{
return false;
}
}
}
思路三:
快指针走到末尾,慢指针刚好到中间。其中慢指针将前半部分反转。然后比较。
要找单链表的中间节点,用快慢指针最好,只用遍历一遍
慢指针一次走一步,快指针一次走两步,找到中心点(通过快慢指针找到中心点)
快指针和慢指针从同一位置点出发
/**
* 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 boolean isPalindrome(ListNode head) {
ListNode kuai = head, pre = null, preper = null;
ListNode man = head;
pre = head;
while(kuai != null && kuai.next != null){
// 将慢指针经过的地方进行链表翻转
// 链表存的都是地址,真正改变的是next的值
pre = man;
man = man.next;
kuai = kuai.next.next;
pre.next = preper;
preper = pre;
}
// man前半部分链表的尾结点
if(kuai != null){
man = man.next;
}
while(pre != null && man != null){
if(pre.val != man.val){
return false;
}
pre = pre.next;
man = man.next;
}
return true;
}
}
链表翻转
链表翻转是对链表的指向进行操作,不是移动链表本身的元素
搜索旋转排序数组
整数数组 nums 按升序排列,数组中的值互不相同。
在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。
给你旋转后的数组nums和一个整数target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。
思路1:暴力枚举法 时间复杂度O(n)
class Solution {
public int search(int[] nums, int target) {
for(int i=0;i<nums.length;i++){
if(nums[i] == target){
return i;
}
}
return -1;
}
}
思路2:这道题和平常二分法查找的不同就在于,把一个有序递增的数组分成了,两个递增的数组,我们需要做的就是判断这个数在哪一个递增的数组中,然后再去用常规的二分法去解决。
// 1. 首先明白,旋转数组后,从中间划分,一定有一边是有序的。
// 2. 由于一定有一边是有序的,所以根据有序的两个边界值来判断目标值在有序一边还是无序一边。
// 3. 这题找目标值,遇到目标值即返回。
// 4. 注意:由于有序的一边的边界值可能等于目标值,所以判断目标值是否在有序的那边时应该加个等号(在二分查找某个具体值得时候如果把握不好边界值,可以再每次查找前判断下边界值,也就是while循环里面的两个if注释)。
class Solution {
public int search(int[] nums, int target) {
int start=0, end=nums.length-1;
// 采用二分查找法
while(start <= end){
int mid = (start + end)/2;
if(nums[mid] == target){
return mid;
}
// 先判断两边 哪一边是有序 哪一边是无序
else if(nums[0] <= nums[mid]){
// 左侧有序
if(nums[mid] > target && target >= nums[start]){
end = mid - 1;
}
else{
start = mid + 1;
}
}
else{
// 右侧有序
if(nums[mid] < target && target <= nums[end]){
start = mid + 1;
}
else{
end = mid - 1;
}
}
}
return -1;
}
}
多数元素
给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
思路一:暴力枚举法。用map记录下所有元素出现的次数
二叉树的最大深度
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
思路一:动态规划法或递归
max{左子树的深度, 右子树的深度}
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public int maxDepth(TreeNode root) {
if(root == null){
return 0;
}
int leftDepth = 0;
int rightDepth = 0;
if(root.left != null){
leftDepth = maxDepth(root.left);
}
if(root.right != null){
rightDepth = maxDepth(root.right);
}
return Math.max(leftDepth+1,rightDepth+1);
}
}
最小路径和
给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
思路一:递归法
class Solution {
public int minPathSum(int[][] grid) {
return pathSum(0,0,grid);
}
public int pathSum(int i, int j, int[][] grid){
int length = grid.length;
int width = grid[0].length;
int lw = 0;
int ll = 0;
if(i+1 < length){
ll = pathSum(i+1,j,grid);
}
else{
ll = -1;
}
if(j+1 < width){
lw = pathSum(i,j+1,grid);
}
else{
lw = -1;
}
if(lw >= 0 && ll >= 0){
return Math.min(lw,ll) + grid[i][j];
}
else if(lw < 0 && ll < 0){
return grid[i][j];
}
else if(lw < 0){
return ll + grid[i][j];
}
else{
return lw + grid[i][j];
}
}
}
思路二:动态规划法
class Solution {
public int minPathSum(int[][] grid) {
for(int i=0;i<grid.length;i++){
for(int j=0;j<grid[0].length;j++){
if(j==0 && i== 0){
continue;
}
else if(i-1 >= 0 && j-1>=0){
grid[i][j] = Math.min(grid[i-1][j],grid[i][j-1]) + grid[i][j];
}
else if(i-1 < 0){
grid[i][j] = grid[i][j-1] + grid[i][j];
}
else{
grid[i][j] = grid[i-1][j] + grid[i][j];
}
}
}
return grid[grid.length-1][grid[0].length-1];
}
}
盛最多水的容器
给定一个长度为 n的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。
找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
返回容器可以储存的最大水量。
说明:你不能倾斜容器。
思路一:采用暴力枚举法,计算每个点的最大容纳量,
缺点:耗时长
class Solution {
public int maxArea(int[] height) {
int out = 0;
for(int i=0; i<height.length; i++){
for(int j=i+1; j<height.length; j++){
out = Math.max(out, Math.min(height[i],height[j]) * (j-i));
}
}
return out;
}
}
思路二:双向双指针法
在每个状态下,无论长板或短板向中间收窄一格,都会导致水槽 底边宽度 -1−1 变短:
若向内 移动短板 ,水槽的短板可能变大,因此下个水槽的面积 可能增大 。
若向内 移动长板 ,水槽的短板不变或变小,因此下个水槽的面积 一定变小 。
因此,初始化双指针分列水槽左右两端,循环每轮将短板向内移动一格,并更新面积最大值,直到两指针相遇时跳出;即可获得最大面积。
class Solution {
public int maxArea(int[] height) {
int out = 0;
int flag1 = 0, flag2 = height.length-1;
while(flag1 < flag2){
if(height[flag1] < height[flag2]){
out = Math.max(out, (flag2-flag1)*height[flag1]);
flag1++;
}
else{
out = Math.max(out, (flag2-flag1)*height[flag2]);
flag2--;
}
}
return out;
}
}
回文子串 +0320++++++++++++++++
给你一个字符串s,请你统计并返回这个字符串中 回文子串 的数目。
回文字符串 是正着读和倒过来读一样的字符串。
子字符串 是字符串中的由连续字符组成的一个序列。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
思路一:暴力枚举法
列出所有的子串
判断自串是否为回文子串(从开始和结束位置,采用双指针进行判断)
思路二:采用动态规划法-----遍历I j 记录之前的操作
class Solution {
public int countSubstrings(String s) {
// 动态规划法
boolean[][] dp = new boolean[s.length()][s.length()];
int ans = 0;
for (int j = 0; j < s.length(); j++) {
for (int i = 0; i <= j; i++) {
if (s.charAt(i) == s.charAt(j) && (j - i < 2 || dp[i + 1][j - 1])) {
dp[i][j] = true;
ans++;
}
}
}
return ans;
}
}
思路三:中心扩展法
class Solution6472 {
public int countSubstrings(String s) {
// 中心扩展法
int ans = 0;
for (int center = 0; center < 2 * s.length() - 1; center++) {
// left和right指针和中心点的关系是?
// 首先是left,有一个很明显的2倍关系的存在,其次是right,可能和left指向同一个(偶数时),也可能往后移动一个(奇数)
// 大致的关系出来了,可以选择带两个特殊例子进去看看是否满足。
int left = center / 2;
int right = left + center % 2;
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
ans++;
left--;
right++;
}
}
return ans;
}
}
汉明距离
两个整数之间的 汉明距离 指的是这两个数字对应二进制位不同的位置的数目。
给你两个整数 x 和 y,计算并返回它们之间的汉明距离。
思路一:直接异或,计算1的个数(异或 加 移位)
java 异或 ^ 移位 >>
class Solution {
public int hammingDistance(int x, int y) {
int flag = 0;
int out = x^y;
while(out>0){
if(out % 2 != 0){
flag++;
}
out = out / 2;
}
return flag;
}
}
思路二:
class Solution {
public int hammingDistance(int x, int y) {
int s = x ^ y, ret = 0;
while (s != 0) {
ret += s & 1;
s >>= 1;
}
return ret;
}
}
任务调度器+0321++++++++++++++++
给你一个用字符数组 tasks 表示的 CPU 需要执行的任务列表。其中每个字母表示一种不同种类的任务。任务可以以任意顺序执行,并且每个任务都可以在 1 个单位时间内执行完。在任何一个单位时间,CPU 可以完成一个任务,或者处于待命状态。
然而,两个 相同种类 的任务之间必须有长度为整数 n 的冷却时间,因此至少有连续 n 个单位时间内 CPU 在执行不同的任务,或者在待命状态。
你需要计算完成所有任务所需要的 最短时间 。
思路一:
存在空闲时间和不存在空闲时间两种状态
class Solution {
public int leastInterval(char[] tasks, int n) {
int[] zz = new int[26];
int taskNum = 0;
int maxTaskNum = 0;
for(char cc:tasks){
zz[cc - 'A']++;
taskNum = Math.max(taskNum, zz[cc - 'A']);
}
for(int flag:zz){
if(flag == taskNum){
maxTaskNum++;
}
}
return Math.max(tasks.length, (taskNum - 1)*(n+1) + maxTaskNum);
}
}
爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
思路一:
思路二:动态规划法
class Solution {
public int climbStairs(int n) {
int[] dp = new int[n+1];
dp[0] = 1;
dp[1] = 1;
for(int i=2; i <= n; i++){
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
}
二叉树的中序遍历
给定一个二叉树的根节点 root ,返回 它的 中序 遍历 。
DLR--前序遍历(根在前,从左往右,一棵树的根永远在左子树前面,左子树又永远在右子树前面 )
LDR--中序遍历(根在中,从左往右,一棵树的左子树永远在根前面,根永远在右子树前面)
LRD--后序遍历(根在后,从左往右,一棵树的左子树永远在右子树前面,右子树永远在根前面)
思路一:采用递归法 进行输出
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> out = new ArrayList<Integer>();
List<Integer> leftOut = new ArrayList<Integer>();
List<Integer> rightOut = new ArrayList<Integer>();
if(root == null){
return out;
}
if(root.left != null){
leftOut = inorderTraversal(root.left);
}
if(root.right != null){
rightOut = inorderTraversal(root.right);
}
out.addAll(leftOut);
out.add(root.val);
out.addAll(rightOut);
return out;
}
}
删除链表的倒数第 N 个结点
思路一:
采用两遍遍历
第一遍:找链表总长度
第二遍:删除倒数第n个节点
思路二:
采用双指针法--------一遍遍历
两指针间隔n个节点,每次都步进1
/**
* 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 removeNthFromEnd(ListNode head, int n) {
ListNode fast = head;
ListNode slow = null;
int flag = 1;
while(fast.next != null){
fast = fast.next;
if(flag == n){
slow = head;
}
else if(flag > n){
slow = slow.next;
}
flag++;
}
if(slow != null ){
if(slow.next != null){
slow.next = slow.next.next;
}
else{
slow.next = null;
}
return head;
}
else{
return head.next;
}
}
}
统计封闭岛屿的数目
二维矩阵 grid 由 0 (土地)和 1 (水)组成。岛是由最大的4个方向连通的 0 组成的群,封闭岛是一个 完全 由1包围(左、上、右、下)的岛。
请返回 封闭岛屿 的数目。
思路一:采用深度优先遍历
四个方向深搜, 有一个方向越界就不是封闭的
必须要将四个方向都跑完,不能遇到一个为false就返回
使用深度优先搜索方法。从土地即0的位置进行深搜,在深搜过程中,每次搜索到0位置时将其改成1防止重复搜索。本题难点是如何判断搜索到的岛屿是否为封闭岛屿,可以不难发现,只要是封闭岛屿,那么就不会搜索到矩阵的边界,通过这一个条件,可以在搜索过程中返回一个bool值,表示该岛屿是否为封闭岛屿。
class Solution {
public int closedIsland(int[][] grid) {
int num = 0;
if(grid.length < 3 || grid[0].length < 3){
return num;
}
for(int i=1; i < grid.length - 1; i++){
for(int j=1; j < grid[0].length - 1; j++){
if(grid[i][j] == 0){
if(dfs(grid, i, j)){
num++;
}
}
}
}
return num;
}
public boolean dfs(int[][] grid, int i, int j){
if((i == 0 || i == grid.length - 1 || j == 0 || j == grid[0].length - 1) && grid[i][j] == 0){
return false;
}
// 防止回头搜索已经搜索过的 两个岛屿将必定间隔1
grid[i][j] = 1;
int[] dx = {-1, 0, 1, 0};
int[] dy = {0, -1, 0, 1};
boolean out = true;
for(int k=0; k<4; k++){
int ii = i + dx[k];
int jj = j + dy[k];
if(ii < 0 || ii > grid.length - 1 || jj < 0 || jj > grid[0].length - 1 || grid[ii][jj] == 1){
continue;
}
out = out & dfs(grid, ii, jj);
}
return out;
}
}
将数组分成和相等的三个部分
给你一个整数数组 arr,只有可以将其划分为三个和相等的 非空 部分时才返回 true,否则返回 false。
形式上,如果可以找出索引 i + 1 < j 且满足 (arr[0] + arr[1] + ... + arr[i] == arr[i + 1] + arr[i + 2] + ... + arr[j - 1] == arr[j] + arr[j + 1] + ... + arr[arr.length - 1]) 就可以将数组三等分。
思路一:看能不能将数组进行三等分 采用贪心法 找最小的数组长度
class Solution {
public boolean canThreePartsEqualSum(int[] arr) {
int out = 0;
int out2 = 0;
for(int i=0; i < arr.length; i++){
out = out + arr[i];
}
int flag = out / 3;
int flag2 = 0;
if(out % 3 == 0 && arr.length >= 3){
for(int i=0; i < arr.length - 1; i++){
out2 = out2 + arr[i];
if(out2 == flag){
flag2++;
out2 = 0;
}
// 具备跳出机制 采用贪心法 找最小的数组长度
if(flag2 == 2){
return true;
}
}
}
return false;
}
}
思路二:使用双指针,从数组两头开始一起找,节约时间
左右两边都等于 sum/3 ,中间也一定等于
将数组拆分成斐波那契序列
给定一个数字字符串 num,比如 "123456579",我们可以将它分成「斐波那契式」的序列 [123, 456, 579]。
形式上,斐波那契式 序列是一个非负整数列表 f,且满足:
0 <= f[i] < 231 ,(也就是说,每个整数都符合 32 位 有符号整数类型)
f.length >= 3
对于所有的0 <= i < f.length - 2,都有 f[i] + f[i + 1] = f[i + 2]
另外,请注意,将字符串拆分成小块时,每个块的数字一定不要以零开头,除非这个块是数字 0 本身。
返回从 num 拆分出来的任意一组斐波那契式的序列块,如果不能拆分则返回 []。
思路一:回朔法+剪枝法
根据斐波那契式序列的要求,从第 33 个数开始,每个数都等于前 22 个数的和,因此从第 33 个数开始,需要判断拆分出的数是否等于前 22 个数的和,只有满足要求时才进行拆分,否则不进行拆分。
交换和
给定两个整数数组,请交换一对数值(每个数组中取一个数值),使得两个数组所有元素的和相等。
返回一个数组,第一个元素是第一个数组中要交换的元素,第二个元素是第二个数组中要交换的元素。若有多个答案,返回任意一个均可。若无满足条件的数值,返回空数组。
思路:
(建立公式进行分析)
以一个值为出发点,找另一个数组中找满足条件的值
难点在另一个数组中的查找上面(进行排序,再二分查找)
Arrays.sort(array1)
利用Set的快速查找, 同时set具有不可重复性
手写快排
class Solution {
public int[] findSwapValues(int[] array1, int[] array2) {
int num1 = 0,num2 = 0;
// 利用set记录出现的数据
Set<Integer> container = new HashSet<>();
for(int j : array1){
num1 += j;
}
for(int j : array2){
num2 += j;
container.add(j);
}
if((num1 + num2) % 2 != 0){
return new int[]{};
}
for(int j : array1){
if(container.contains(j + (num2-num1)/2)){
return new int[]{j, j + (num2-num1)/2};
}
}
return new int[]{};
}
}
写字符串需要的行数
我们要把给定的字符串 S 从左到右写到每一行上,每一行的最大宽度为100个单位,如果我们在写某个字母的时候会使这行超过了100 个单位,那么我们应该把这个字母写到下一行。我们给定了一个数组 widths ,这个数组 widths[0] 代表 'a' 需要的单位, widths[1] 代表 'b' 需要的单位,..., widths[25] 代表 'z' 需要的单位。
现在回答两个问题:至少多少行能放下S,以及最后一行使用的宽度是多少个单位?将你的答案作为长度为2的整数列表返回。
思路1:遍历整个字符串进行 行统计 以及 最后行的单位
class Solution {
public int[] numberOfLines(int[] widths, String s) {
char[] cc = s.toCharArray();
int len = 0, num = 1;
for(char c : cc){
int val = widths[c - 'a'];
if(len + val > 100){
num += 1;
len = val;
}
else{
len += val;
}
}
return new int[]{num,len};
}
}
思路二:
class Solution {
public static final int MAX_WIDTH = 100;
public int[] numberOfLines(int[] widths, String s) {
int lines = 1;
int width = 0;
for (int i = 0; i < s.length(); i++) {
int need = widths[s.charAt(i) - 'a'];
width += need;
if (width > MAX_WIDTH) {
lines++;
width = need;
}
}
return new int[]{lines, width};
}
}
检查数组对是否可以被 k 整除
给你一个整数数组 arr 和一个整数 k ,其中数组长度是偶数,值为 n 。
现在需要把数组恰好分成 n / 2 对,以使每对数字的和都能够被 k 整除。
如果存在这样的分法,请返回 True ;否则,返回 False 。
思路:
利用余数进行配对,对于第一次取余小于0的元素来说,只要再加上一个k就能使其大于0
class Solution {
public boolean canArrange(int[] arr, int k) {
List<Integer> list = new ArrayList<>();
for(int i : arr){
int flag;
if(i % k < 0){
// 针对数组中存在小于0的数
flag = (i % k + k) % k;
}
else{
flag = i % k;
}
if(flag > 0){
list.add(flag);
}
}
if(list.size() % 2 != 0){
return false;
}
Collections.sort(list);
for(int i=0; i<list.size()/2; i++){
if(list.get(i) + list.get(list.size() - i - 1) != k){
return false;
}
}
return true;
}
}
重复叠加字符串匹配+0327++++++++++++++++
给定两个字符串 a 和 b,寻找重复叠加字符串 a 的最小次数,使得字符串 b 成为叠加后的字符串 a 的子串,如果不存在则返回 -1。
注意:字符串 "abc" 重复叠加 0 次是 "",重复叠加 1 次是 "abc",重复叠加 2 次是 "abcabc"。
思路:
先确定上界和下界
下界:a的复制长度大于等于b的长度,才有可能匹配
上界:主串由a复制过来,且从主串中找到子串b,因此子串的起始位置不会超过a的长度
class Solution {
public int repeatedStringMatch(String a, String b) {
StringBuilder sb = new StringBuilder();
int num = 0;
while(sb.length() < b.length()){
sb.append(a);
num++;
}
sb.append(a);
// 用于检索 StringBuild中是否存在子串 出现子串的首次位置
int index = sb.indexOf(b);
if(index == -1){
return -1;
}
return index + b.length() > a.length() * num ? num+1:num;
}
}
KMP 算法是一个快速查找匹配串的算法,它的作用其实就是本题问题:如何快速在「原字符串」中找到「匹配字符串」的下标。
排列硬币
你总共有 n 枚硬币,并计划将它们按阶梯状排列。对于一个由 k 行组成的阶梯,其第 i 行必须正好有 i 枚硬币。阶梯的最后一行 可能 是不完整的。
给你一个数字 n ,计算并返回可形成 完整阶梯行 的总行数。
思路一:
等差序列
二分查找计算 n 枚硬币
思路二
思路三:暴力
class Solution {
public int arrangeCoins(int n) {
int i = 0;
long total = 0;
while(total<n){
i++;
total += i;
}
if (total == n){
return i;
}
else{
return i-1;
}
}
}