leetcode题目思考与记录

91 阅读24分钟

简略地描述思路,以供后来观之

本笔记只是备忘录,并不是详细解决方案,请使用ctrl+F查询

Algorithm

1. 打印字符串全排列

递归后需要马上还原(因为所有的),因为递归是一颗深度优先的树(也就是栈),是先从叶子开始改变的

2. 二叉搜索树转为双向链

中序遍历二叉搜索树正好是递增的,只需要把结点左右指针连接中序中前后的结点。(二叉搜索树没有相同值的点)

3. 递归求排列

求排列时若用递归法,递归叶子回溯到父亲时需要还原改变(叶子最先改变,也就是说是从字符串数组最后两位开始的),不然就不是同一串字符串出发的(画出排列树推吧)

3. 复制复杂链表

每个结点复制一个接在该结点后面,a->a'->b->b'->c->c',同时复制原结点所指向的任意结点,最后奇偶结点分离(跳着分离)

4. 最小的k个数

partition函数(快排)最后得到的index就是他在序列中的位置,左边的就是前index个数。(期间可以用二分法逐渐缩小区间,由于是用一个固定位置的数作pivot所以最后得到的index是针对整个数组而言的)

5. 集合和最大堆基于红黑树

6. 最大子数组和

如果累加和为负则重新从新元素开始

7. 从1到n数里包含1的个数

O(log10N) , 十位上,每轮出现10次是指[10,19]这10个数 解析

8. 逆序对

归并排序的同时进行计数

9. 数组排成最小的数

可以用sprintf将数字写入字符串

10. 二叉树最低公共祖先

(若是二叉搜索树)比一个结点大,比另一个结点小;(若有指向父节点的指针)寻找两个链表的公共结点;(什么都不是)用栈递归到叶子再逐个往上退栈。

11. 二叉树路径和等于某个数

传递vector的引用(用vector作栈方便顺序输出),递归遍历,进入结点时压入该结点,退出时弹出(恢复成父节点进入的情景)

12. 不能继承的类

虚拟继承virtual 父类,把父类构造函数设为私有,并且父类含有一个友元函数,友元函数的类型是由模板类给出的,所以虚拟继承的时候需要把子类的类型当成参数传入;(只能在堆上)私有构造与析构,用静态getInstance函数返回一个新的父类对象。

13. C++按类中声明的元素顺序初始化成员

14. 求方程正整数解个数

求方程正整数解个数

15. 多路平衡归并排序

外排序,m个归并项(子表个数)k路归并的归并趟数s=logk(m),败者树

16. n阶乘末尾0的个数

(n除以5,即求5的质因数个数) 存在0必然是52的结果,所以必然含有5!(5432*1),所以只要求多少个5的倍数即可

17. mongodb为什么没有自增id

因为并发情况下同时插入,分布式存储的情况下无法协调分配连续数字(协调需要消耗网络资源),并且内部也不是按id顺序存的,因为文档长度可变,不知道什么时候文档变长要放到后面去

18. 数n的约数个数

(约数:能把别人整除的叫约数)自然数的约数的个数是有限的,质数的约数是1和本身;合数一定有3个以上的约数。由于约数个数定理知道:约数的个数等于:所有质因数的指数加上1后的乘积; 若一个数分解质因数后为(a^m)*(b^n),其中a,b均为质因数;m,n均为相应质因数的指数. 则约数个数为(m+1)(n+1).(因为a^0,a^1 ... a^m,都是它的约数共m+1个)

例如: (1)12=2^2 * 3,质因数有2和3,其指数分别为2和1,那么12的约数有(2+1) *(1+1)=6(个); (2)60=22 * 3 * 5,质因数2, 3, 5的指数分别为2, 1, 1,那么60的约数有(2+1) * (1+1) * (1+1)=12(个)

19. 一个数所有约数之和

一个数所有约数之和 等于先把每个质因数从0次幂一直加到其最高次幂,再把每个相应质因数幂的和相乘. 若一个数分解为(a^m)*(b^n),则这个数所有约数的和为: (a^0+a^1+a^2+a^3+…+a^m)(b^0+b^1+b^2+b^3+…+b^n).

例如: (1)12=223,则12所有约数的和为:(2^0+2^1+2^2)(3^0+3^1)=74=28; (2)60=2235=(2^0+2^1+2^2)(3^0+3^1)(5^0+5^1)=74*6=168.

20. Trie 树

21. 需要同时移动多少步A和B才同时指向一个节点

长度为100的循环链表,指针A和指针B都指向了链表中的同一个节点,A以步长为1向前移动,B以步长为3向前移动,一共需要同时移动多少步A和B才能再次指向同一个节点?

  • 假定经过n步A、B再次相遇。则A经过的结点为n,B经过的结点为3n;此刻B必然比A多经过了整数倍的链表长度(圈的长度),假定经过了i倍的链表长度,则有3n-n=100i,即2n=100i;满足该等式的最小整数位i=2,n=100。即A经过了100个结点,B经过了300个结点,二者再次相遇。
    • 也可以认为跑圈100m,A速度1m/s B速度3m/s,B比A多跑一圈耗时100/(3-1)=50s

22. 模拟加减乘

注意 ^按位异或& 按位与| 按位或

加法运算

将一个整数用二进制表示,其加法运算就是:相异(^)时,本位为1,进位为0;同为1时本位为0,进位为1;同为0时,本位进位均为0。

故不计进位的和为sum = a^b,进位就是arr = a&b,(与sum相加时先左移一位,因为这是进位)。完成加法直到进位为0.

int Add(int a, int b){
    return b ? Add(a^b, (a&b)<<1) : a;
}

求相反数

//求a的相反数:将各位取反加一
int negative(int a){     //get -a
    return Add(~a, 1);
}

减法运算

a-b = a+(-b)   根据补码的特性,各位取反加1即可(注意得到的是相反数,不是该数的补码,因为符号位改变了) (上面用二进制实现的加减法可以直接应用于负数)

int Minus(int a, int b)
{
    return Add(a, negative(b));
}

乘法运算

原理上还是通过加法计算。将b个a相加 参考二进制数转十进制数,每位有一个2次幂的权重,当该位为1时,加上 2^x 倍 a 所以权重 a 不断 a<<=1

//正数乘法
int Multi(int a, int b){
    int ans = 0;
    while(b){
        if(b&1)
            ans = Add(ans, a);
        a = a << 1;
        b = b >> 1;
    }
    return ans;
}
//正数除法
int Divide(int a, int b)
{
    int coun = 0;
    while(a >= b)
    {
        a = Minus(a, b);
        coun = Add(coun, 1);
    }
    return coun;
}

除法运算

除法运算是乘法的逆。看a最多能减去多少个b,

//判断是否是负数,0,正数
int isneg(int a)
{
    return a & 0x8000;
}
int iszero(int a)
{
    return !(a & 0xFFFF);
}
int ispos(int a)
{
    return (a&0xFFFF) && !(a&0x8000);
}

//处理负数的乘法和除法
int My_Multi(int a, int b)
{
    if(iszero(a) || iszero(b))
        return 0;
    if(isneg(a))
    {
        if(isneg(b))
            return Multi(negative(a), negative(b));
        else
            return negative(Multi(negative(a), b));
    }else if(isneg(b))
        return negative(Multi(a, negative(b)));
    else
        return Multi(a, b);
}

int My_Divide(int a, int b)
{
    if(iszero(b))
    {
        cout << "Error!" << endl;
        exit(1);
    }
    if(iszero(a))
        return 0;
    if(isneg(a))
    {
        if(isneg(b))
            return Divide(negative(a), negative(b));
        else
            return negative(Divide(negative(a), b));
    }else if(isneg(b))
        return negative(Divide(a, negative(b)));
    else
        return Divide(a, b);
}

计算统中数值一律用补码来表示,因为补码可以使符号位和数值位统一处理,同时可以使减法按照加法来处理。 原码 -> 补码: 数值位取反加1 数值位不包括符号位 补码 -> 原码: 对该补码的数值位继续 取反加1 补码的绝对值:(称为真值)正数的真值就是本身,负数的真值是各位(包括符号位)取反加1(即变成原码并把符号位取反) b -> -b : 各位(包括符号位)取反加1

旋转字符:字符串前面的部分排在后面

"abcd"的旋转串是"abcd","bcda" ... "dabc" 首先目标串与原串长度要相等

  • 可以设字符s+s,只要是s+s的子串都是旋转字符 比如:"abcd"+"abcd",即"abcdabcd"中寻找子串,时间复杂度是根据查找子串的时间来定的,所以如果是采用KMP则是O(N)

字符串提取出现的正负整数总和

注意可以负负得正 设置符号位positive,cur当前字符,num每一位不断往高位挪的积累数(字符串转化为数字),res数字总和

  • 当cur为数字时,num = num * 10 + positive? cur:-cur;
  • 当cur不为数字的时候,说明此时刚刚结束数字的转化,或者是开始部分没有数字,无论哪种都应该加上刚刚转化的数字num,并把num置零重新开始计数 此时需要注意当cur为负号时,前一位为负则positive=true,否则为负;如果不是负号,则重置positive为正

字符串转换成int范围内数字

字符串可以是只出现一个0,即 0 字符串不能出现 - -0 -02 A12 如果没有出现违法,只要检查是否每个字符都是数字就可以,

  • 接下来进入判断,由于int范围是-21474836482147483647 先把所有的数字都转为负数(转为负数可以不用再写一遍正数的,最后如果是整数再负负得正回去)
  • 判断是否溢出,因为-214748364 * 10 + (-8) 刚好是最大整数 先判断是否 ( res < = -214748364 )|| (res == -214748364 && cur <= -8 ) 同时也要注意如果是正整数,则转换后不能大于 2147483647

字符串replace a with b

for(for...){
	if(a[i++]==b[j]){ 
		j++ ;
		if(j==源串长){ // 不用源串长-1,前面++过了
			标记a串 [i-源串长,i] 为0或者某个特定符表示需要删掉
			//(为了节省时间我们可以不删掉,只是不打印就可以)
		}
	}
	else j=0;
}

当a[i]=0而a[i-1]!=0时表示可以开始将前面积累的字符串打印了 res = res + cur + to 注意最后是如果末尾全是0,没有到不是0的过度,我们需要再手动把这部分转换加上 res += cur

计算连续出现字符的个数

aabbbcc -> a_2_b_3_c2 这种连续计数,在字符变化后结束上一轮计数的都是一种做法,注意末尾部分没有转换需要手动加上

  • 本题从第二个字符开始判断每个字符和前一个字符如果不一样,进入转换,记录数字个数,重置计数个数为1(因为当转换出现,和前个不一样,前个计数为1,例如ab,从b开始,a计数为1)

如果是 a_2_b_3_c2 -> aabbbcc,求第5个字符是什么,此处是b 由于有"_",可以因此分别数字与字符,保存当前字符cur,记录数字,当数字大于需要求的位置则返回

判断是否每个字符只出现一次

可以用Map 但是如果只花费O(1)空间,只能使用堆排序,将数组排序对比前后是否相同

浮点数高精度幂

替换空为%20

先扫描一遍记录总长度为len+2*blank,再从后往前复制

一般挪动数组元素的,都需要从右往左复制

reverse字符串

reverse整个串

while(start<end){ swap(c[start],c[end]); start++; end--; }

reverse前后部分顺序

abcde->deabc 先翻转abc,再翻转de,再翻转整个串

变成回文串需要的最少字符

str长为N,NNN*N 的动态规划表 dp[i][j]表示str[i..j]这段字符最少需要多个字符使str[i..j]变成回文串

dp[i][j]=

{0,i=j0,str[i]=str[j]andij=21,str[i]!=str[j]andij2dp[i+1]dp[j1],str[i]=str[j]andij2maxdp[i+1][j],dp[i][j1]+1,str[i]==str[j]andij2 \begin{cases} 0, & i=j \\ 0 , & str[i]=str[j] \quad and \quad |i-j|=2 \\ 1 , & str[i]!=str[j] \quad and \quad|i-j|\geq2 \\ dp[i+1]dp[j-1] , & str[i]=str[j] \quad and \quad |i-j|\geq2 \\ max{dp[i+1][j],dp[i][j-1]}+1 , & str[i]==str[j] \quad and \quad |i-j|\geq2&\end{cases}

未完待续

判断最长左右括号

()()(())合法,返回最长的合法出现的括号位置

  • 先检查()的数量是否相等
  • 设dp[i]表示str[0..i]位置上,以str[i]结尾的最长有效括号长度 ....(()())6....(()())为6
    1. str[i]==(,有效括号长度子串以)结尾,故dp[i]=0;
    2. str[i]==),dp[i-1]代表str[i-1]结尾的最长有效括号长度,上一处没有被匹配过的位置i-1-dp[i-1]上如果是(,可以和str[i]凑成一对,此时dp[i]=dp[i-1]+2,同时dp[i-1]前面即str[i-2]之前的dp[i-2]也应该算上
  • 最后求解 Max(dp[0..N-1]) 就是最终结果

计算字符串 4*((3*(-4)+-4*3))的结果

每次碰到(就进入递归进入下一层,)就结束递归,并传递参数 (char [], 下一个位置位置 )数组返回(计算的结果, 结束的位置)

Recur(char [] , int i){
	while(i < length && ch[i] !=')'){
		if( ch[i] 是数字 ){
			pre = pre * 10 + ch[i] - '0';
			i++;
		}else if (ch[i] 是操作符 ){
			队列a入队pre
			队列a入队ch[i]
			pre = 0;
			i++;
		}else(ch[i] == '('){
			array = 递归本函数(ch,i+1)
			pre = array[0];
			i = array[1]+1;
		}
	}
}

整数N的二进制全排中0左边必有1的个数

按顺序拼接字符串使总结果字典序最小

  • 先按照拼接的方式将数组排列,即 return (a+b)>(b+a)
  • 将排序后数组从头到尾组合起来

贪心算法,有详细需证明它的贪心策略是正确

新类型字符a、Aa、AA 中第k个字符是哪种类型的

从k-1个字符开始往左对大写字母计数,碰到小写字母停止

  • 根据奇偶判断kk+1是AA,还是Aa,还是k+1是单独的a

回文段最少切割数

dp[i]是子串str[i..len-1]需要切几次才能使str[i..len-1]全切成回文串,dp[0]是最后结果

从右往左,i初始为len-1

  • 如果 str[i..j]是回文串,dp[i]=dp[j+1]+1,只需从str[j+1..len-1]寻找最经济的切割法
  • j 从 [i..len-1] 遍历,dp[i]=Min{dp[j+1]+1},i<=j<len且str[i..j]为回文串
  • 快速判断是否是回文串的方法是回文子串长度的相同方法,设p[i][j]为i..j间是否是回文串的判定 以下情况p[i][j]是回文串:
    • str[i..j]长度为1

    • str[i..j]长度为2,且2个字符相等

    • 子串str[i+1..j-1]是回文串(即p[i][j]=true),且str[i]==str[j],最外面的两个字符串相等。

    • i 是从右往左,j是从i往右,所以i..j间长度是展开的便于计算p[i][j]

移动0

二叉树的中序遍历

爬楼梯、括号生成

删除排序数组中的重复项

盛水最多的容器

跳表

trim树 - 搜索自动补全

TSP问题

三重dp

拓扑 + ?

单调栈

括号生成

大根堆

PriorityQueue默认是小跟堆

背包问题为什么不能换顺序?组合和排序有什么不同? 卡特兰数是什么 ⭐

零钱兑换II和爬楼梯问题到底有什么不同?

快排优化

  1. 42. 接雨水

  2. 84. 柱状图中最大的矩形

  3. 保卫王国 - 牛客

归并排序

剑指 Offer 51. 数组中的逆序对和 [315. 计算右侧小于当前元素的个数]都是归并排序的相同方法,区别在于左指针还是右指针入队时判断

归并排序优化

  • 当递归到规模足够小时,利用插入排序(归并排序只是需要两个有序数组,小于7的时候使用插入排序可以更节省栈空间)
  • 归并前判断一下是否还有必要归并(如果左边的最大的比右边的最小的还小(或者等于),那就不用归并了,已经有序了)
  • 只在排序前开辟一次空间(如文中所述, System.copyOfRange每次会分配空间,System.arraycopy 只是每次在已经开辟的空间上复制 )
方法一 递归归并
    public int reversePairs(int[] arr) {
        int[] temp = new int[arr.length];
        return sort(arr, 0, arr.length - 1, temp);
    }

    // 变成 [left, mid] [mid + 1, right]的闭区间
    public int sort(int[] arr, int left, int right, int[] temp) {
        if (left >= right) return 0;
        // mid + 1的时候left可能会比right大1

        int mid = (right - left) / 2 + left; // 防止溢出
        int leftReverse = sort(arr, left, mid, temp);
        int rightReverse = sort(arr, mid + 1, right, temp);

        // 对已经排序后,两个有序数组来说,此处发现已经有序,不用继续
        if (arr[mid] <= arr[mid + 1]) return leftReverse + rightReverse;
        return mergeS(arr, left, mid, right, temp) + leftReverse + rightReverse;
    }

    private int mergeS(int[] arr, int l, int m, int r, int[] temp) {
        int total = r - l + 1;
        // 拷贝一遍当前排序状态的[l, r]当作辅助数组
        // 一个用于指针遍历,一个用于存放结果
        // 这里是修改了原数组存放结果,拷贝的辅助数组来遍历指针
        System.arraycopy(arr, l, temp, l, total);

        int reverseNum = 0;
        int p = l;
        int q = m + 1;
        for (int i = l; i <= r; i++) {
            if (p > m) {    // 左区间指针已经走完
                arr[i] = temp[q++];
                // 已经没有
            } else if (q > r) { // 右区间指针已经走完
                arr[i] = temp[p++];
            } else if (temp[p] <= temp[q]) { // 辅助数组大于等于时,归并排序才是稳定排序(指相同数字依然按照原数组顺序排序)
                arr[i] = temp[p++];  // 左区间更小
            } else {  // 右区间更小
                arr[i] = temp[q++];
                // 右指针前进时,左区间尚存的数都是该数都逆序数(两数组各自有序,左区间尚存的数肯定比右指针之前一个指向的数大)
                reverseNum += m - p + 1;
            }
        }
        return reverseNum;
    }

354. 俄罗斯套娃信封问题

长宽都需要大于另一个信封时才能被套娃

  1. 按长升序,宽降序

长相同的信封不能套娃,所以长相同时需要按宽降序,忽略相同的长:使用LIS寻找不连续的上升序列的时候可以自动忽略相同的长

  1. 使用LIS对宽进行LIS,寻找不连续的上升序列

最长连续序列

并查集,用v和v+1并组,每一个数字a的parent就是并查集里该组对应的最大数字b,b - a + 1 就是的距离取max

public String[] largestNumberList(int[] nums) {
    // Get input integers as strings.
    String[] asStrs = new String[nums.length];
    for (int i = 0; i < nums.length; i++) {
        asStrs[i] = String.valueOf(nums[i]);
    }

    // 把问题丢给string...
    Arrays.sort(asStrs, (a, b) -> (b + a).compareTo(a + b)); // 降序

    // 特别地,所有数都为0的情况下,返回0(因为前导0是要去掉的)
    if (asStrs[0].equals("0")) {
        return "0";
    }
    return asStr;
}

46. 全排列

总个数是卡特兰数 方法一,回溯,不重复元素下标,然后对应两个位置交换。这种不保证字典序,保证字典序需要预先对list排序

public void recursion(List<Integer> a, int k) {
    if (index == nums.length) {
        res.add(new ArrayList<>(a));
        return;
    }
    for (int i = 0; i < total; i++) {
        for (int j = i; j < total; j++) {
            swap(a, i, j); // 注意重要的点在这里
            recursion(a, k + 1);
            swap(a, j, i);
        }
    }
}

方法二,保证字典序,for循环每个位置选a里的数组,找出所有组合,然后用used去重

    public List<List<Integer>> permute(int[] nums) {
        allComposeWithNoMap(0, nums, new ArrayList<>());
        return resAll;
    } 
    
    private void allComposeNext(int index, int[] nums, List<Integer> res, Set<Integer> used) {
        if (index == nums.length) {
            resAll.add(new ArrayList<>(res));
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            if (used.contains(nums[i])) continue;
            res.add(nums[i]);
            used.add(nums[i]);
            allComposeNext(index + 1, nums, res, used);
            res.remove(res.size() - 1);
            used.remove(nums[i]);
        }
    }

329. 矩阵中的最长递增路径

  • 从某个位置出发的最大步数是固定的。
  • 由于只用记步数,所以dfs四个方向最大的步数可以记忆化记下来
  • 经过自己需要++1
  • memo记忆化数组不为空,表示搜索过以这个点出发

也可以用BFS+拓扑排序 设(i,j)比相邻点大,出度为0,这就是边界条件 计算全图出度,从所有出度为0的点开始BFS 每遍历一个点,就拓扑排序去掉一个出度,若出度减为0,则入队BFS 结果为BFS层序遍历的次数

螺旋矩阵

可以构造visited[]数组记录去过的地方,然后用dir[]数组记录要走的方向,这个会比较通用一点的解法。 简单解法是构造(l, t, r, b)方框,每走完一边就缩小范围,以下两个方法都是这样做的

方法一
//    ---------
//    |       |
//    |       |
//    --------|
    private static List<Integer> calcBounds(int[][] matrix) {
        List<Integer> res = new ArrayList<>();
        int row = matrix.length;
        int col = matrix[0].length;
        int l = 0, t = 0, b = row - 1, r = col - 1;
        while (true) {
            for (int i = l; i <= r; i++) res.add(matrix[t][i]);
            
            if (++t > b) break;
            for (int i = t; i <= b; i++) res.add(matrix[i][r]);
            
            if (--r < l) break;
            for (int i = r; i >= l; i--) res.add(matrix[b][i]);
            
            if (--b < t) break;
            // 此处t已经下挪过一了
            for (int i = b; i >= t; i--) res.add(matrix[i][l]);
            if (++l > r) break;
        }
        return res;
    }
}
方法二
//    ---------
//    |       |
//    |       |
//    |-------|
    private static List<Integer> calcBounds1(int[][] matrix) {
        List<Integer> order = new ArrayList<Integer>();
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
            return order;
        }
        int rows = matrix.length, columns = matrix[0].length;
        int l = 0, r = columns - 1, t = 0, b = rows - 1;

        while (l <= r && t <= b) {
            for (int column = l; column <= r; column++) {
                order.add(matrix[t][column]);
            }
            for (int row = t + 1; row <= b; row++) {
                order.add(matrix[row][r]);
            }
            if (l < r && t < b) {
                for (int column = r - 1; column > l; column--) {
                    order.add(matrix[b][column]);
                }
                for (int row = b; row > t; row--) {
                    order.add(matrix[row][l]);
                }
            }
            // 统一缩小
            l++;
            r--;
            t++;
            b--;
        }
        return order;
    }

约瑟夫环问题

见此

矩阵最长递增路径

同一个单元格对应的最长递增路径的长度是固定不变的。 所以记忆化搜索从memo[i][j]开始的最长递增路径

无损压缩算法:deflate、Huffman Coding

寻找两个有序数组的中位数

要求算法的时间复杂度为 O(log(m + n)) 即找出第(m+n)/2个元素 设k=(m+n)/2,则需要在两个数组中分别寻找第k/2个元素,原因如下:

题目中要求的时间复杂度为 O(log(m + n)),很容易想到的方法就是二分,现在有两个数组,要对那个数组进行二分合适?由于找的是中位数,那么这个数字的两边的元素个数是相等的,所以只需要确定一个数组中的两边元素,两一个数组的对应的补上去就可以了,为了提高效率,要选择最短的数组做二分查找 — 原文

为什么Java中int型数据取值范围是[-2^{31}, 2^{31}-1]

为什么Java中int型数据取值范围是[-2^{31}, 2^{31}-1]

判断一个有符号int型整数是不是溢出:

x > INT_MAX / 10 || x == INT_MAX && x * 10 + 7 就会溢出 x < INT_MIN / 10 || x == INT_MIN. && x * 10 + 8 就会溢出

反转数字

push一位数字x:int res = res * 10 + x Pop一位数字:res /= 10 需要警惕是否溢出

Lru最近访问

Lru在Java里就是LinkedHashMap有序字典,每次get/put后把该元素挪动到双向链表尾。双向链表是因为方便删除,是所有数据的。

2、3、5硬币面值(无限个数),构成总数为k的方法数

和上楼梯很像,区别在于上楼梯只是1、2两步

上楼梯的状态方程: dp[0] = dp[1] = 1, dp[i] = dp[i-1] + dp[i-2] (i > 2)。设置dp[0] = 1只是为了后续好加; 这一题就是dp[1] = 1, dp[i] = dp[i-2] + dp[i-3] + dp[i-5],意义是为了总数为i的金额是由i-2的情况和i-3i-5的情况达成的

300. 最长上升子序列 LIS

方法一 动态规划

dp[i]保存[0, i]的最长上升子序列 (可以不连续) 中找到的一个比当前数i小的数j,如果第j个数字小于第i个数字,i就可以跟在j后面 dp[i] = Math.max(dp[i], dp[j] + 1)0 <= j < i

    public int lengthOfLIS(int[] nums) {
        int total = nums.length;
        int[] dp = new int[total];
        // Arrays.fill(dp, 1);
        dp[0] = 1; //注意都要初始化为1,因为自己就是一个长度为1的序列
        int max = 1;
        for (int i = 1; i < total; i++) {
            dp[i] = 1; 
            for (int j = 0; j < i; j++) {
                if (nums[i] > nums[j]) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
            max = Math.max(max, dp[i]);
        }
        return max;
    }
方法二 贪心 + 二分

要使上升子序列尽可能的长,得让序列上升得尽可能慢,在上升子序列最后加上的那个数尽可能的小。 基于上面的贪心思路,维护一个数组 d[i] ,录目前最长上升子序列的长度,遍历数组看是否能二分插入其中获得最小值 最后数组的长度就是LIS

    public int lengthOfLIS(int[] nums) {
        int total = nums.length;
        int len = 0; // len = 下次要插入的地方
        int[] a = new int[total + 1]; // minElements 保存所有最小的上升数字,如果更小的数字可以插入到这个数组里,则替换掉
        for (int i = 0; i < total; i++) {
            // find where to insert in minElements 寻找在比它更小的数字后面插入
            int l = 0, r = len - 1;
            while (l <= r) {
                int mid = (r + l) >> 1;
                if (a[mid] < nums[i]) {
                    l = mid + 1; // 插入到这个数后
                } else {
                    r = mid - 1;
                }
            }
            // every element is larger than m[i], clear the array and set to m[0] 没找到比它更小的数字
            a[l] = nums[i];
            if (l == len) { // 比所有位置都大,需要在后面新加一个
                len++;
            }
        }
        return len;
    }

击鼓传花

dp
第 i 个同学,可以收到第(i-1)个同学或者第(i+1)个同学的花。 dp[i][j]=dp[(i-1)][(j-1+n)%n]+dp[(i-1)][(j+1+n)%n];

击鼓传花 - 动态规划

字符压缩算法(字符串解码)

(括号匹配) 用栈

反转链表II


    public static class SolutionReverseLinkedListNodeII {
        //思路:head表示需要反转的头节点,pre表示需要反转头节点的前驱节点
        //我们需要反转n-m次,我们将head的next节点移动到需要反转链表部分的首部,需要反转链表部分剩余节点依旧保持相对顺序即可
        //比如1->2->3->4->5,m=1,n=5
        //第一次反转:1(head) 2(next) 3 4 5 反转为 2 1 3 4 5
        //第二次反转:2 1(head) 3(next) 4 5 反转为 3 2 1 4 5
        //第三次发转:3 2 1(head) 4(next) 5 反转为 4 3 2 1 5
        //第四次反转:4 3 2 1(head) 5(next) 反转为 5 4 3 2 1
        public ListNode reverseBetween(ListNode head, int m, int n) {
            ListNode dummy = new ListNode(0);
            dummy.next = head;
            ListNode prev = dummy;
            while (m > 1) {
                prev = prev.next;
                m--;
                n--;
            }
            head = prev.next;

            // 1 2 3 4 5
            while (n > 1) {
                ListNode next = head.next; //    3 (head指向的元素一直不变)
                head.next = head.next.next;   //     1-> 4
                next.next = prev.next;   //   3 -> 2 (prev.next是上一次的next,永远是反转部分的第一个,因为prev的位置不变)
                prev.next = next;  //  0 -> 2
                n--;
            }
            return dummy.next;
        }
    }

73. 矩阵置零

问题在于如何提醒是0 方法一:扫一遍有0的地方,额外开两个数组记下i和j。再遍历一次将出现的地方置0 方法二:扫一遍有0的地方标记为一个特殊值,再次遍历一次将特殊值出现的地方置0 方法三:由于可能不知道特殊值设置为什么,所以把出现0的地方对应的第一行第一列标记为0。第一行第一列需要一开始就判断有没有0。 最后根据第一行第一列的是否有0来置0。

推荐阅读:

  1. 【阿里】算法工程师笔试题整理(13&14年)

  2. 剑指offer题目及答案