LeetCode算法笔记(一)

 


 

1.两数之和

暴力法:
class
Solution { public int[] twoSum(int[] nums, int target) { for (int i = 0; i < nums.length; i++) { for (int j = i + 1; j < nums.length; j++) { if (nums[j] == target - nums[i]) { return new int[] { i, j }; } } } throw new IllegalArgumentException("No two sum solution"); } }
两遍哈希表
class
Solution {
public int[] twoSum(int[] nums, int target) {
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < nums.length; i++) {
            map.put(nums[i], i);
        }
        for (int i = 0; i < nums.length; i++) {
            int complement = target - nums[i];
            if (map.containsKey(complement) && map.get(complement) != i) {
                return new int[] { i, map.get(complement) };
            }
        }
        throw new IllegalArgumentException("No two sum solution");
    }
}
一遍哈希表
class
Solution { public int[] twoSum(int[] nums, int target) { Map<Integer, Integer> map = new HashMap<>(); for (int i = 0; i < nums.length; i++) { int complement = target - nums[i]; if (map.containsKey(complement)) { return new int[] { map.get(complement), i }; } map.put(nums[i], i); } throw new IllegalArgumentException("No two sum solution"); } }

 2.两数之和

给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。

如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。假设除了数字 0 之外,这两个数都不会以 0 开头。

public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
    ListNode dummyHead = new ListNode(0);
    ListNode p = l1, q = l2, curr = dummyHead;
    int carry = 0;
    while (p != null || q != null) {
        int x = (p != null) ? p.val : 0;
        int y = (q != null) ? q.val : 0;
        int sum = carry + x + y;
        carry = sum / 10;
        curr.next = new ListNode(sum % 10);
        curr = curr.next;
        if (p != null) p = p.next;
        if (q != null) q = q.next;
    }
    if (carry > 0) {
        curr.next = new ListNode(carry);
    }
    return dummyHead.next;
}

这里用到了一个链表游标的功能。

3.字符串相乘

给定两个以字符串形式表示的非负整数 num1 和 num2,返回 num1 和 num2 的乘积,它们的乘积也表示为字符串形式。

class Solution {
    public String multiply(String num1, String num2) {
        /**
        num1的第i位(高位从0开始)和num2的第j位相乘的结果在乘积中的位置是[i+j, i+j+1]
        例: 123 * 45,  123的第1位 2 和45的第0位 4 乘积 08 存放在结果的第[1, 2]位中
          index:    0 1 2 3 4  
              
                        1 2 3
                    *     4 5
                    ---------
                          1 5
                        1 0
                      0 5
                    ---------
                      0 6 1 5
                        1 2
                      0 8
                    0 4
                    ---------
                    0 5 5 3 5
        这样我们就可以单独都对每一位进行相乘计算把结果存入相应的index中        
        **/
        
        int n1 = num1.length()-1;
        int n2 = num2.length()-1;
        if(n1 < 0 || n2 < 0) return "";
        int[] mul = new int[n1+n2+2];
        
        for(int i = n1; i >= 0; --i) {
            for(int j = n2; j >= 0; --j) {
                int bitmul = (num1.charAt(i)-'0') * (num2.charAt(j)-'0');      
                bitmul += mul[i+j+1]; // 先加低位判断是否有新的进位
                
                mul[i+j] += bitmul / 10;
                mul[i+j+1] = bitmul % 10;
            }
        }
        
        StringBuilder sb = new StringBuilder();
        int i = 0;
        // 去掉前导0
        while(i < mul.length-1 && mul[i] == 0) 
            i++;
        for(; i < mul.length; ++i)
            sb.append(mul[i]);
        return sb.toString();
    }
}

4.无重复字符的最长字串

给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。

暴力法:

思路:逐个检查所有的子字符串,看它是否不含有重复的字符。

算法:假设我们有一个函数 boolean allUnique(String substring) ,如果子字符串中的字符都是唯一的,它会返回 true,否则会返回 false。 我们可以遍历给定字符串 s 的所有可能的子字符串并调用函数 allUnique。 如果事实证明返回值为 true,那么我们将会更新无重复字符子串的最大长度的答案。

现在让我们填补缺少的部分:

1.为了枚举给定字符串的所有子字符串,我们需要枚举它们开始和结束的索引。假设开始和结束的索引分别为 i 和 j。那么我们有0≤i<j≤n(这里的结束索引 j 是按惯例排除的)。因此,使用 i 从 0 到 n - 1以及 j从 i+1到 n这两个嵌套的循环,我们可以枚举出 s 的所有子字符串。

2.要检查一个字符串是否有重复字符,我们可以使用集合。我们遍历字符串中的所有字符,并将它们逐个放入 set 中。在放置一个字符之前,我们检查该集合是否已经包含它。如果包含,我们会返回 false。循环结束后,我们返回 true。

public class Solution {
    public int lengthOfLongestSubstring(String s) {
        int n = s.length();
        int ans = 0;
        for (int i = 0; i < n; i++)
            for (int j = i + 1; j <= n; j++)
                if (allUnique(s, i, j)) ans = Math.max(ans, j - i);
        return ans;
    }

    public boolean allUnique(String s, int start, int end) {
        Set<Character> set = new HashSet<>();
        for (int i = start; i < end; i++) {
            Character ch = s.charAt(i);
            if (set.contains(ch)) return false;
            set.add(ch);
        }
        return true;
    }
}

解题思路:考虑使用滑动窗口方法,通过使用 HashSet 作为滑动窗口,我们可以用 O(1)的时间来完成对字符是否在当前的子字符串中的检查。

滑动窗口是数组/字符串问题中常用的抽象概念。 窗口通常是在数组/字符串中由开始和结束索引定义的一系列元素的集合,即 [i, j)左闭,右开)。而滑动窗口是可以将两个边界向某一方向“滑动”的窗口。例如,我们将 [i, j)向右滑动 1 个元素,则它将变为 [i+1, j+1)(左闭,右开)。

回到我们的问题,我们使用 HashSet 将字符存储在当前窗口 [i, j)(最初 j = i)中。 然后我们向右侧滑动索引 j,如果它不在 HashSet 中,我们会继续滑动 j。直到 s[j] 已经存在于 HashSet 中。此时,我们找到的没有重复字符的最长子字符串将会以索引 i 开头。如果我们对所有的 i这样做,就可以得到答案。

public class Solution {
    public int lengthOfLongestSubstring(String s) {
        int n = s.length();
        Set<Character> set = new HashSet<>();
        int ans = 0, i = 0, j = 0;
        while (i < n && j < n) {
            // try to extend the range [i, j]
            if (!set.contains(s.charAt(j))){
                set.add(s.charAt(j++));
                ans = Math.max(ans, j - i);
            }
            else {
                set.remove(s.charAt(i++));
            }
        }
        return ans;
    }
}

优化的滑动窗口
上述的方法最多需要执行 2n 个步骤。事实上,它可以被进一步优化为仅需要 n 个步骤。我们可以定义字符到索引的映射,而不是使用集合来判断一个字符是否存在。 当我们找到重复的字符时,我们可以立即跳过该窗口。也就是说,如果 s[j] 在 [i, j)范围内有与 j'重复的字符,我们不需要逐渐增加 i 。 我们可以直接跳过 [i,j'] 范围内的所有元素,并将 i 变为 j' + 1

public class Solution {
    public int lengthOfLongestSubstring(String s) {
        int n = s.length(), ans = 0;
        Map<Character, Integer> map = new HashMap<>(); // current index of character
        // try to extend the range [i, j]
        for (int j = 0, i = 0; j < n; j++) {
            if (map.containsKey(s.charAt(j))) {
                i = Math.max(map.get(s.charAt(j)), i);
            }
            ans = Math.max(ans, j - i + 1);
            map.put(s.charAt(j), j + 1);
        }
        return ans;
    }
}

扩展:Java(假设字符集为 ASCII 128)

以前的我们都没有对字符串 s 所使用的字符集进行假设。

当我们知道该字符集比较小的时侯,我们可以用一个整数数组作为直接访问表来替换 Map。

常用的表如下所示:

  • int [26] 用于字母 ‘a’ - ‘z’ 或 ‘A’ - ‘Z’
  • int [128] 用于ASCII码
  • int [256] 用于扩展ASCII码
public class Solution {
    public int lengthOfLongestSubstring(String s) {
        int n = s.length(), ans = 0;
        int[] index = new int[128]; // current index of character
        // try to extend the range [i, j]
        for (int j = 0, i = 0; j < n; j++) {
            i = Math.max(index[s.charAt(j)], i);
            ans = Math.max(ans, j - i + 1);
            index[s.charAt(j)] = j + 1;
        }
        return ans;
    }
}

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

给定两个大小为 m 和 n 的有序数组 nums1 和 nums2。

请你找出这两个有序数组的中位数,并且要求算法的时间复杂度为 O(log(m + n))。

你可以假设 nums1 和 nums2 不会同时为空。

方:创建一个数组长度为mid+1的数组nums,如果两个数组的长度为偶数,则结果为(nums[mid-1]+nums[mid])/2,如果两个数组的长度为奇数,则结果为nums[mid];
nums数组创建的思路:比较两个数组,按照从小到大的顺序排列。
class
Solution { public double findMedianSortedArrays(int[] nums1, int[] nums2) { int len = nums1.length + nums2.length; int mid = len/2; int flag = 0; if(len%2==0){ flag = 1; } int[] nums = new int[mid + 1]; int i = 0, j = 0; for(int k = 0;k < mid+1; k++){ if(i < nums1.length && j < nums2.length){ nums[k] = nums1[i] < nums2[j]? nums1[i++]: nums2[j++]; }else if(i < nums1.length && j >= nums2.length){ nums[k] = nums1[i++]; }else if(j < nums2.length && i >= nums1.length){ nums[k] = nums2[j++]; } } if(flag > 0){ return (nums[mid-1] + nums[mid]) / 2.0; } return nums[mid]; } }

class
Solution { public double findMedianSortedArrays(int[] A, int[] B) { int m = A.length; int n = B.length; if (m > n) { // to ensure m<=n int[] temp = A; A = B; B = temp; int tmp = m; m = n; n = tmp; } int iMin = 0, iMax = m, halfLen = (m + n + 1) / 2; while (iMin <= iMax) { int i = (iMin + iMax) / 2; int j = halfLen - i; if (i < iMax && B[j-1] > A[i]){ iMin = i + 1; // i is too small } else if (i > iMin && A[i-1] > B[j]) { iMax = i - 1; // i is too big } else { // i is perfect int maxLeft = 0; if (i == 0) { maxLeft = B[j-1]; } else if (j == 0) { maxLeft = A[i-1]; } else { maxLeft = Math.max(A[i-1], B[j-1]); } if ( (m + n) % 2 == 1 ) { return maxLeft; } int minRight = 0; if (i == m) { minRight = B[j]; } else if (j == n) { minRight = A[i]; } else { minRight = Math.min(B[j], A[i]); } return (maxLeft + minRight) / 2.0; } } return 0.0; } }

6.最长回文子串

给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。

方:这里使用窗口滑动方法,求最长回文,窗口大小考虑从大到小设置,首先让窗口大小设置为s.length(),逐个比较两边元素是否相等。
class
Solution { public String longestPalindrome(String s) { if(s.length() == 1 || "".equals(s)) return s; int len = s.length(); int subLen = len; while(subLen>0){ for(int i = 0; i + subLen <= len; i++){ String subStr = s.substring(i, i + subLen); int midLen = (subLen + 1) / 2; for(int k = 0; k < midLen; k++){ if(subStr.charAt(k) != subStr.charAt(subLen - k - 1)){ break; } if(k == midLen-1){ return subStr; } } } subLen--; } return ""; } }
扩展中心法:
public
String longestPalindrome(String s) { if (s == null || s.length() < 1) return ""; int start = 0, end = 0; for (int i = 0; i < s.length(); i++) { int len1 = expandAroundCenter(s, i, i); int len2 = expandAroundCenter(s, i, i + 1); int len = Math.max(len1, len2); if (len > end - start) { start = i - (len - 1) / 2; end = i + len / 2; } } return s.substring(start, end + 1); } private int expandAroundCenter(String s, int left, int right) { int L = left, R = right; while (L >= 0 && R < s.length() && s.charAt(L) == s.charAt(R)) { L--; R++; } return R - L - 1;//这里-1是因为这个时候的R和L超出了回文的范围,且这个时候已经不是回文了 }

public String Manacher(String s) {
        if (s.length() < 2) {
            return s;
        }
        StringBuilder sb = new StringBuilder();
        sb.append("^");
        for(int i=0; i < s.length(); i++){
            sb.append("#" + s.charAt(i));
        }
        sb.append("#$");
        String t = sb.toString();
        
        int len = t.length();
        int[] p = new int[len];
        int mx = 0, id = 0, maxLength=-1;
        int index = 0;
        for(int i = 1; i < len - 1; i++){
            p[i] = mx > i? Math.min(p[2*id-i], mx-i):1;
            while(t.charAt(i + p[i]) == t.charAt(i - p[i])){
                p[i]++;
            }
            if(mx < i + p[i]){
                mx = i + p[i];
                id = i;
            }
            if(maxLength < p[i] - 1 ){
                index = i;
                maxLength = p[i] - 1;
            }
        }
        int start = (index - p[index]) / 2;
        return s.substring(start, start + maxLength);
    }

 

 

 7.Z字形变换

将一个给定字符串根据给定的行数,以从上往下、从左到右进行 Z 字形排列。

思路:通过从左向右迭代字符串,我们可以轻松地确定字符位于 Z 字形图案中的哪一行。
算法:我们可以使用min(numRows,len(s)) 个列表来表示 Z 字形图案中的非空行。
从左到右迭代s,将每个字符添加到合适的行。可以使用当前行和当前方向这两个变量对合适的行进行跟踪。
只有当我们向上移动到最上面的行或向下移动到最下面的行时,当前方向才会发生改变。
public String convert(String s, int numRows) {
if (numRows == 1) return s;

        List<StringBuilder> rows = new ArrayList<>();
        for (int i = 0; i < Math.min(numRows, s.length()); i++)
            rows.add(new StringBuilder());

        int curRow = 0;
        boolean goingDown = false;

        for (char c : s.toCharArray()) {
            rows.get(curRow).append(c);
            if (curRow == 0 || curRow == numRows - 1) goingDown = !goingDown;
            curRow += goingDown ? 1 : -1;
        }

        StringBuilder ret = new StringBuilder();
        for (StringBuilder row : rows) ret.append(row);
        return ret.toString();
    }
思路:按照与逐行读取 Z 字形图案相同的顺序访问字符串。
算法:首先访问 行 0 中的所有字符,接着访问 行 1,然后 行 2,依此类推...
对于所有整数 k,
行 0 中的字符位于索引 k(2⋅numRows−2) 处;
行numRows−1 中的字符位于索引 k(2⋅numRows−2)+numRows−1 处;
内部的 行 i 中的字符位于索引 k(2⋅numRows−2)+i 以及(k+1)(2⋅numRows−2)−i 处;
class Solution {
public String convert(String s, int numRows) {

        if (numRows == 1) return s;

        StringBuilder ret = new StringBuilder();
        int n = s.length();
        int cycleLen = 2 * numRows - 2;

        for (int i = 0; i < numRows; i++) {
            for (int j = 0; j + i < n; j += cycleLen) {
                ret.append(s.charAt(j + i));
                if (i != 0 && i != numRows - 1 && j + cycleLen - i < n)
                    ret.append(s.charAt(j + cycleLen - i));
            }
        }
        return ret.toString();
    }
}

 8.整数反转

给出一个 32 位的有符号整数,你需要将这个整数中每位上的数字进行反转。

注意:假设我们的环境只能存储得下 32 位的有符号整数,则其数值范围为 [−2^31,  2^31 − 1]-->[-2147483648, 2147483647]。请根据这个假设,如果反转后整数溢出那么就返回 0。

该算法定义了一个Long int类型数据,Long是一个64位的整数,因此,可以存放下32位有符号整数。先使用Long存储,最后结果判断范围。
该算法因为定义了一个Long类型,需要占用更多的内存。
public
int reverse(int x) { Long result = 0L ; for(;x!=0;x=x/10){ result = result*10+x%10; } return result>Integer.MAX_VALUE || result<Integer.MIN_VALUE? 0:result.intValue();//转换为int型
}
这里将int先转换为String类型,且会判断正负和最后一位是否为0;判断方法是取出String中的char字符,并进行比较。String-->StringBuilder,reverse()-->int.
此时可能会转换出错,因为超出int范围,这里我用try-catch进行回传,这种算法不合适
public
int reverse(int x){ if(x < 10 && x > -10) return x; String result = String.valueOf(x); boolean fu = false; if(result.charAt(0) == '-'){ fu = true; result = result.substring(1); } if(result.charAt(result.length() - 1) == '0'){ result = result.substring(0, result.length() - 1); } StringBuilder sb = new StringBuilder(result); sb = sb.reverse(); result = sb.toString(); if(fu){ try{ return -Integer.parseInt(result); }catch(Exception e){ return 0; } } try{ return Integer.parseInt(result); }catch(Exception e){ return 0; } }

 


方法:弹出和推入数字 & 溢出前进行检查
思路:我们可以一次构建反转整数的一位数字。在这样做的时候,我们可以预先检查向原整数附加另一位数字是否会导致溢出。


算法:反转整数的方法可以与反转字符串进行类比。


我们想重复“弹出”xx 的最后一位数字,并将它“推入”到rev 的后面。最后,rev 将与 x相反。


要在没有辅助堆栈/数组的帮助下 “弹出” 和 “推入” 数字,我们可以使用数学方法。


//pop operation:
pop = x % 10;
x /= 10;

//push operation:
temp = rev * 10 + pop;
rev = temp;  

但是,这种方法很危险,因为当 temp=rev⋅10+pop 时会导致溢出。


幸运的是,事先检查这个语句是否会导致溢出很容易。


为了便于解释,我们假设rev 是正数。


如果temp=rev⋅10+pop 导致溢出,那么一定有rev≥ INTMAX /10 。
如果rev> INTMAX/10,那么 temp=rev⋅10+pop 一定会溢出。
如果rev== INTMAX/10,那么只要pop > 7, temp=rev⋅10+pop 就会溢出。

rev 为负时可以应用类似的逻辑。

class Solution {

  public int reverse(int x) {

int rev = 0;
        while (x != 0) {
            int pop = x % 10;
            x /= 10;
            if (rev > Integer.MAX_VALUE/10 || (rev == Integer.MAX_VALUE / 10 && pop > 7)) return 0;
            if (rev < Integer.MIN_VALUE/10 || (rev == Integer.MIN_VALUE / 10 && pop < -8)) return 0;
            rev = rev * 10 + pop;
        }
        return rev;
    }
} 

 9.字符串转换整数

实现一个 atoi 函数,使其能将字符串转换成整数。

首先,该函数会根据需要丢弃无用的开头空格字符,直到寻找到第一个非空格的字符为止。

当我们寻找到的第一个非空字符为正或者负号时,则将该符号与之后面尽可能多的连续数字组合起来,作为该整数的正负号;假如第一个非空字符是数字,则直接将其与之后连续的数字字符组合起来,形成整数。

该字符串除了有效的整数部分之后也可能会存在多余的字符,这些字符可以被忽略,它们对于函数不应该造成影响。

注意:假如该字符串中的第一个非空格字符不是一个有效整数字符、字符串为空或字符串仅包含空白字符时,则你的函数不需要进行转换。

在任何情况下,若函数不能进行有效的转换时,请返回 0。

说明:假设我们的环境只能存储 32 位大小的有符号整数,那么其数值范围为 [−2^31,  2^31 − 1]。如果数值超过这个范围,请返回  INT_MAX (231 − 1) 或 INT_MIN (−231) 。

class Solution {
    public int myAtoi(String str) {
        str = str.trim();//针对"      "这种情况
        if("".equals(str)) return 0;//先处理一次空字符串        
        boolean fu = false;
        if(str.charAt(0) == '-' || str.charAt(0) == '+') {
            if(str.charAt(0) == '-') fu = true;
            str = str.substring(1);
        }
        if("".equals(str)) return 0;//针对"-", "+"
        int k = 0;
        if(str.charAt(0) > 57 || str.charAt(0) < 48) return 0;//除符号外,如果首字符是字符,则返回0
        while(k < str.length()){
            if(str.charAt(k) > 57 || str.charAt(k) < 48) break;//先数字,后字符的情况
            k++;
        }
        str = str.substring(0, k);        
        try{//这里用try-catch处理字符越界的情况
            return fu? -Integer.parseInt(str):Integer.parseInt(str);
        }catch(Exception e){
            return fu? Integer.MIN_VALUE: Integer.MAX_VALUE; 
        }
    }
}
public class Solution {

    public int myAtoi(String str) {
        int len = str.length();

        // 去除前导空格
        int index = 0;
        while (index < len) {
            if (str.charAt(index) != ' ') {
                break;
            }
            index++;
        }

        if (index == len) {
            return 0;
        }

        // 第 1 个字符如果是符号,判断合法性,并记录正负
        int sign = 1;
        char firstChar = str.charAt(index);
        if (firstChar == '+') {
            index++;
            sign = 1;
        } else if (firstChar == '-') {
            index++;
            sign = -1;
        }

        // 不能使用 long 类型,这是题目说的
        int res = 0;
        while (index < len) {
            char currChar = str.charAt(index);
            // 判断合法性
            if (currChar > '9' || currChar < '0') {
                break;
            }

            // 题目中说:环境只能存储 32 位大小的有符号整数,因此,需要提前判断乘以 10 以后是否越界
            if (res > Integer.MAX_VALUE / 10 || (res == Integer.MAX_VALUE / 10 && (currChar - '0') > Integer.MAX_VALUE % 10)) {
                return Integer.MAX_VALUE;
            }
            if (res < Integer.MIN_VALUE / 10 || (res == Integer.MIN_VALUE / 10 && (currChar - '0') > -(Integer.MIN_VALUE % 10))) {
                return Integer.MIN_VALUE;
            }

            // 每一步都把符号位乘进去
            res = res * 10 + sign * (currChar - '0');
            index++;
        }

        return res;
    }
}
class Solution {
    public int myAtoi(String str) {
        int len = str.length();
        int index = 0;
        while(index < len){
            if(str.charAt(index) != ' '){
                break;
            }
            index++;
        }
        if(index == len) return 0;

        int sign = 1;
        char firstChar = str.charAt(index);
        if(firstChar == '-'){
            index++;
            sign = -1;
        }else if(firstChar == '+'){
            index++;
            sign = 1;
        }
        int result = 0;
        while(index < len){
            int pop = str.charAt(index) - '0';
            if(pop > 9 || pop < 0) break;
            if(result > Integer.MAX_VALUE/10 || (result == Integer.MAX_VALUE/10 && pop > Integer.MAX_VALUE % 10)) return Integer.MAX_VALUE;
            if(result < Integer.MIN_VALUE/10 || (result == Integer.MIN_VALUE/10 && pop > -(Integer.MIN_VALUE % 10))) return Integer.MIN_VALUE;
            result = result*10 + sign*pop;
            index++;
        }
        return result;
    }
}

10.回文数

判断一个整数是否是回文数。回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。

class Solution {
    public boolean isPalindrome(int x) {
        if(x < 0 || (x%10==0 && x > 0)) return false;
        int result = 0;
        
        while(x > result){
            result = result*10 + x % 10;
            x /= 10;
        }

        return (result == x) || (x == result/10);
    }
}

11.正则表达式匹配

12.盛最多水的容器

给定 n 个非负整数 a1,a2,...,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0)。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

说明:你不能倾斜容器,且 n 的值至少为 2。

 

暴力法
class
Solution { public int maxArea(int[] height) { if(height.length < 2) return 0; int len = height.length; int maxArea = 0; for(int i = 0; i < len - 1; i++){ for(int j = 0; j < len; j++){ maxArea = Math.max(maxArea, (j - i)*Math.min(height[i], height[j])); } } return maxArea; } }
左右指针法,保留大的指针
class
Solution { public int maxArea(int[] height) { int l = 0, maxArea = 0, r = height.length - 1; while(l < r){ maxArea = Math.max(maxArea, (r-l)*Math.min(height[l], height[r])); if(height[l] > height[r]){ r--; }else{ l++; } } return maxArea; } }
左右指针算法,相比上面,多了一个中间指针,优势在于不需要每选择一个区间都进行面积计算,中间指针可以使得只有面积比原来大才进行面积计算。
class
Solution { public int maxArea(int[] height) { int l = 0, maxArea = 0, r = height.length - 1, mid=0; while(l < r){ mid = height[l] > height[r]? r: l; maxArea = Math.max(maxArea, (r-l)*Math.min(height[l], height[r])); if(height[l] > height[r]){ r--; while(height[r] <= height[mid]){ r--; if(l >= r){ return maxArea; } } }else { l++; while(height[l] <= height[mid]){ l++; if(l >= r){ return maxArea; } } } } return maxArea; } }

13.整数转罗马数字

罗马数字包含以下七种字符: I, V, X, LCD 和 M

 

例如, 罗马数字 2 写做 II ,即为两个并列的 1。12 写做 XII ,即为 X + II 。 27 写做  XXVII, 即为 XX + V + II 。

通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如 4 不写做 IIII,而是 IV。数字 1 在数字 5 的左边,所表示的数等于大数 5 减小数 1 得到的数值 4 。同样地,数字 9 表示为 IX。这个特殊的规则只适用于以下六种情况:

I 可以放在 V (5) 和 X (10) 的左边,来表示 4 和 9。
X 可以放在 L (50) 和 C (100) 的左边,来表示 40 和 90。 
C 可以放在 D (500) 和 M (1000) 的左边,来表示 400 和 900。

给定一个整数,将其转为罗马数字。输入确保在 1 到 3999 的范围内。

贪心算法:本题“整数转罗马数字”也有类似的思想:在表示一个较大整数的时候,“罗马数字”的设计者不会让你都用 11 加起来,我们总是希望写出来的“罗马数字”的个数越少越好,以方便表示,并且这种表示方式还应该是唯一的。
class Solution {
    public String intToRoman(int num) {
        if(num < 1 || num > 3999) return "";
        int[] nums = new int[]{1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1};
        String[] roms = new String[]{"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"};
        StringBuilder sb = new StringBuilder();
        int index = 0;
        while(index < 13){
            while(num >= nums[index]){
                sb.append(roms[index]);
                num -= nums[index];
            }            
            index++;
        }
        return sb.toString();       

    }
}
暴力法:输入的数据最多4位,创建4个数组,得到每位数字,选择对应的表示形式。
/**
if(num < 1 || num > 3999) return ""; int[] nums = new int[]{1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1}; String[] roms = new String[]{"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"}; StringBuilder sb = new StringBuilder(); int index = 0; while(index < 13){ while(num >= nums[index]){ sb.append(roms[index]); num -= nums[index]; } index++; } return sb.toString();*/ if(num < 1 || num > 3999) return ""; //String[] zeros = new String[]{"", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX"}; //String[] ones = new String[]{"", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC"}; //String[] twos = new String[]{"", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM"}; //String[] threes = new String[]{"", "M", "MM", "MMM"}; String[][] romans = new String[][]{{"", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX"}, {"", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC"}, {"", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM"}, {"", "M", "MM", "MMM", "", "", "", "", "", ""}}; //int zero = 0, one = 0, two = 0, three = 0; int pop = 0; StringBuilder sb = new StringBuilder(); for(int i = 0; i < 4; i++){ pop = num % 10; num /= 10; sb.insert(0, romans[i][pop]); } /*zero = num % 10; num /= 10; one = num % 10; num /= 10; two = num %10; num /= 10; three = num % 10; StringBuilder sb = new StringBuilder(); sb.append(threes[three]); sb.append(twos[two]); sb.append(ones[one]); sb.append(zeros[zero]);*/ return sb.toString(); }

 14.罗马数字转整数

罗马数字包含以下七种字符: I, V, X, L,C,D 和 M。

字符   数值
I      1
V     5
X     10
L      50
C     100
D     500
M     1000
使用一个map存储所有特殊的罗马数字,逐个读取roman数字,当遇到'C', 'X', 'I'这些可能造成歧义的数字时往后多看一位,并进行判断是否存在于map中。
class
Solution { public int romanToInt(String s) { Map<String, Integer> map = new HashMap<>(); map.put("M", 1000); map.put("CM", 900); map.put("D", 500); map.put("CD", 400); map.put("C", 100); map.put("XC", 90); map.put("L", 50); map.put("XL", 40); map.put("X", 10); map.put("IX", 9); map.put("V", 5); map.put("IV", 4); map.put("I", 1); int len = s.length(); int result = 0; for(int i = 0; i < len; i++){ char temp = s.charAt(i); if(i < len -1 && (temp == 'C' || temp == 'X' || temp == 'I')){ String tmp = s.substring(i, i + 2); if(map.containsKey(tmp)){ result += map.get(tmp); i++; }else{ result += map.get(String.valueOf(temp)); } }else{ result += map.get(String.valueOf(temp)); } } return result; } }
该算法在代码实现上,可以往后看多一位,对比当前位与后一位的大小关系,从而确定当前位是加还是减法。当没有下一位时,做加法即可。
也可保留当前位的值,当遍历到下一位的时,对比保留值与遍历位的大小关系,再确定保留值为加还是减。最后一位做加法即可。
class Solution {
public int romanToInt(String s) {
        int sum = 0;
        int preNum = getValue(s.charAt(0));
        for(int i = 1;i < s.length(); i ++) {
            int num = getValue(s.charAt(i));
            if(preNum < num) {
                sum -= preNum;
            } else {
                sum += preNum;
            }
            preNum = num;
        }
        sum += preNum;
        return sum;
    }
    
    private int getValue(char ch) {
        switch(ch) {
            case 'I': return 1;
            case 'V': return 5;
            case 'X': return 10;
            case 'L': return 50;
            case 'C': return 100;
            case 'D': return 500;
            case 'M': return 1000;
            default: return 0;
        }
    }
}

15.最长公共前缀

编写一个函数来查找字符串数组中的最长公共前缀。

如果不存在公共前缀,返回空字符串 ""

暴力法:选择字符串数组中首个字符串为比较字符串,逐个提取该字符串的字符,然后与其他字符串进行比较,发现有不相等的马上退出循环。
class
Solution { public String longestCommonPrefix(String[] strs) { if(strs.length == 0) return "";//[] if(strs.length == 1) return strs[0];//[""]/["as"] int len = strs.length; for(int i = 0; i < len; i++){//针对字符串数组中出现""字符串的情况 if(strs[i].equals("")) return ""; } int index = 0; boolean end = false; char temp; while(true){ if(index < strs[0].length()){//防止越界 temp = strs[0].charAt(index); }else{ break; } for(int i = 1; i < len; i++){//与剩余的字符串中的字符逐个比较 if(index >= strs[i].length() || strs[i].charAt(index) != temp){ end = true; break; } } if(end) break; index++; } return strs[0].substring(0, index); } }

方法一:水平扫描法
思路:首先,我们将描述一种查找一组字符串的最长公共前缀LCP(S1…Sn) 的简单方法。我们将会用到这样的结论:LCP(S1…Sn)=LCP(LCP(LCP(S1,S2),S3),…Sn)
算法:为了运用这种思想,算法要依次遍历字符串[S1…Sn],当遍历到第 ii 个字符串的时候,找到最长公共前缀LCP(S1…Si)。当LCP(S1…Si) 是一个空串的时候,算法就结束了。
否则,在执行了n次遍历之后,算法就会返回最终答案LCP(S
1…Sn)。

public String longestCommonPrefix(String[] strs) { 
  
if (strs.length == 0) return "";
  String prefix
= strs[0];
  
for (int i = 1; i < strs.length; i++)
    
while (strs[i].indexOf(prefix) != 0) {
      prefix
= prefix.substring(0, prefix.length() - 1);
      
if (prefix.isEmpty()) return "";
    }
  
return prefix;
}

算法二:水平扫描


算法:想象数组的末尾有一个非常短的字符串,使用上述方法依旧会进行S​次比较。优化这类情况的一种方法就是水平扫描。我们从前往后枚举字符串的每一列,先比较每个字符串相同列上的字符(即不同字符串相同下标的字符)然后再进行对下一列的比较。

public String longestCommonPrefix(String[] strs) {
    if (strs == null || strs.length == 0) return "";
    for (int i = 0; i < strs[0].length() ; i++){
        char c = strs[0].charAt(i);
        for (int j = 1; j < strs.length; j ++) {
            if (i == strs[j].length() || strs[j].charAt(i) != c)
                return strs[0].substring(0, i);             
        }
    }
    return strs[0];
}
算法三:分治
思路:这个算法的思路来自于LCP操作的结合律。 我们可以发现:LCP(S1…Sn)=LCP(LCP(S1…Sk),LCP(Sk+1…Sn)),其中 LCP(S1…Sn) 是字符串[S1…Sn]的最长公共前缀,1<k<n。
算法:为了应用上述的结论,我们使用分治的技巧,将原问题LCP(Si⋯Sj)分成两个子问题LCP(Si⋯Smid)与LCP(Smid+1,Sj) ,其中 mid=(i+j)/2。
我们用子问题的解 lcpLeft 与 lcpRight 构造原问题的解LCP(Si⋯Sj)。 从头到尾挨个比较 lcpLeft 与 lcpRight 中的字符,直到不能再匹配为止。
计算所得的 lcpLeft 与 lcpRight 最长公共前缀就是原问题的解LCP(Si⋯Sj)。
public
String longestCommonPrefix(String[] strs) { if (strs == null || strs.length == 0) return ""; return longestCommonPrefix(strs, 0 , strs.length - 1); } private String longestCommonPrefix(String[] strs, int l, int r) { if (l == r) { return strs[l]; } else { int mid = (l + r)/2; String lcpLeft = longestCommonPrefix(strs, l , mid); String lcpRight = longestCommonPrefix(strs, mid + 1,r); return commonPrefix(lcpLeft, lcpRight); } } String commonPrefix(String left,String right) { int min = Math.min(left.length(), right.length()); for (int i = 0; i < min; i++) { if ( left.charAt(i) != right.charAt(i) ) return left.substring(0, i); } return left.substring(0, min); }

二分查找法:


这个想法是应用二分查找法找到所有字符串的公共前缀的最大长度 L。 算法的查找区间是(0…minLen),其中minLen是输入数据中最短的字符串的长度,同时也是答案的最长可能长度。 每一次将查找区间一分为二,然后丢弃一定不包含最终答案的那一个。算法进行的过程中一共会出现两种可能情况:


S[1...mid] 不是所有串的公共前缀。 这表明对于所有的 j > i S[1..j] 也不是公共前缀,于是我们就可以丢弃后半个查找区间。


S[1...mid] 是所有串的公共前缀。 这表示对于所有的 i < j S[1..i] 都是可行的公共前缀,因为我们要找最长的公共前缀,所以我们可以把前半个查找区间丢弃。


public String longestCommonPrefix(String[] strs) {
    if (strs == null || strs.length == 0)
        return "";
    int minLen = Integer.MAX_VALUE;
    for (String str : strs)
        minLen = Math.min(minLen, str.length());
    int low = 1;
    int high = minLen;
    while (low <= high) {
        int middle = (low + high) / 2;
        if (isCommonPrefix(strs, middle))
            low = middle + 1;
        else
            high = middle - 1;
    }
    return strs[0].substring(0, (low + high) / 2);
}

private boolean isCommonPrefix(String[] strs, int len){
    String str1 = strs[0].substring(0,len);
    for (int i = 1; i < strs.length; i++)
        if (!strs[i].startsWith(str1))
            return false;
    return true;
}

前缀树:

让我们看一个有些不同的问题:

给定一些键值字符串[S1,S2…Sn],我们要找到字符串 q 与 S 的最长公共前缀。 这样的查询操作可能会非常频繁。

我们可以通过将所有的键值 S 存储到一颗字典树中来优化最长公共前缀查询操作。 如果你想学习更多关于字典树的内容,可以从 208. 实现 Trie (前缀树) 开始。在字典树中,从根向下的每一个节点都代表一些键值的公共前缀。 但是我们需要找到字符串q 和所有键值字符串的最长公共前缀。 这意味着我们需要从根找到一条最深的路径,满足以下条件:

  • 这是所查询的字符串 q 的一个前缀
  • 路径上的每一个节点都有且仅有一个孩子。 否则,找到的路径就不是所有字符串的公共前缀
  • 路径不包含被标记成某一个键值字符串结尾的节点。 因为最长公共前缀不可能比某个字符串本身长

算法:最后的问题就是如何找到字典树中满足上述所有要求的最深节点。 最有效的方法就是建立一颗包含字符串[S1…Sn].


public String longestCommonPrefix(String q, String[] strs) {
    if (strs == null || strs.length == 0)
         return "";  
    if (strs.length == 1)
         return strs[0];
    Trie trie = new Trie();      
    for (int i = 1; i < strs.length ; i++) {
        trie.insert(strs[i]);
    }
    return trie.searchLongestPrefix(q);
}

class TrieNode {

    // 子节点的链接数组
    private TrieNode[] links;

    private final int R = 26;

    private boolean isEnd;

    // 非空子节点的数量
    private int size;    
    public void put(char ch, TrieNode node) {
        links[ch -'a'] = node;
        size++;
    }

    public int getLinks() {
        return size;
    }
    // 假设方法 containsKey、isEnd、get、put 都已经实现了
    // 可以参考文章:https://leetcode.com/articles/implement-trie-prefix-tree/
}

public class Trie {

    private TrieNode root;

    public Trie() {
        root = new TrieNode();
    }

// 假设方法 insert、search、searchPrefix 都已经实现了
// 可以参考文章:https://leetcode.com/articles/implement-trie-prefix-tree/
    private String searchLongestPrefix(String word) {
        TrieNode node = root;
        StringBuilder prefix = new StringBuilder();
        for (int i = 0; i < word.length(); i++) {
            char curLetter = word.charAt(i);
            if (node.containsKey(curLetter) && (node.getLinks() == 1) && (!node.isEnd())) {
                prefix.append(curLetter);
                node = node.get(curLetter);
            }
            else
                return prefix.toString();

         }
         return prefix.toString();
    }
} 

 16.三数之和

给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?找出所有满足条件且不重复的三元组。

注意:答案中不可以包含重复的三元组。

暴力法,超出了时间限制
class
Solution { public List<List<Integer>> threeSum(int[] nums) { Arrays.sort(nums); Set<List<Integer>> result = new HashSet<>(); if(nums == null && nums.length < 3) return Collections.emptyList(); int len = nums.length; for(int i = 0; i < len - 2; i++){ for(int j = i + 1; j < len - 1; j++){ for(int k = j + 1; k < len; k++){ if(nums[i] + nums[j] + nums[k] == 0){ List<Integer> temp = new ArrayList(Arrays.asList(nums[i], nums[j], nums[k])); result.add(temp); } } } } return new ArrayList<List<Integer>>(result); } }

 17.电话号码的字母组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

 

递归法,
class Solution {
    /**private Map<String, String> phone = new HashMap<>(){{
        put("2", "abc");
        put("3", "def");
        put("4", "ghi");
        put("5", "jkl");
        put("6", "mno");
        put("7", "pqrs");
        put("8", "tuv");
        put("9", "wxyz");
    }};**/

    private String[] phone = new String[]{
        "",//0
        "",//1
        "abc",//2
        "def",//3
        "ghi",//4
        "jkl",//5
        "mno",//6
        "pqrs",//7
        "tuv",//8
        "wxyz"//9
        };

    private List<String> result = new ArrayList<>();

    private void combinations(String s, String nextLetters){
        if(nextLetters.length() == 0){
            result.add(s);
            return;
        }
        //String index = nextLetters.substring(0, 1);
        String letters = phone[nextLetters.charAt(0) - '0'];//phone.get(index);
        for(int i = 0; i < letters.length(); i++){
            String combination = letters.substring(i, i + 1);
            combinations(s + combination, nextLetters.substring(1));
        }
    }
    public List<String> letterCombinations(String digits) {
        if(digits == null || digits.length() == 0) return result;

        combinations("", digits);
        return result;

    }
}

18.删除链表的倒数第N个节点

 

给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。

示例:

给定一个链表: 1->2->3->4->5, 和 n = 2.
当删除了倒数第二个节点后,链表变为 1->2->3->5.

说明:给定的 n 保证是有效的。

两次遍历链表,第一次得到链表的长度L,第二次删除第L-n+1个节点。
class
Solution { public ListNode removeNthFromEnd(ListNode head, int n) { ListNode dummpyNode = new ListNode(0); dummpyNode.next = head; ListNode cur = dummpyNode; int count = 0; while(cur != null){ count++; cur = cur.next; } if(count <= n) return null; cur = dummpyNode; for(int i = 0; ; i++){ if(i == count - n - 1){ cur.next = cur.next.next; break; } cur = cur.next; } return dummpyNode.next; } }
一次遍历,设置两个指针,它们在相距n+1,这样,当后面的指针到最后的时候,前面的指针刚好指的是倒数n-1。
class
Solution { public ListNode removeNthFromEnd(ListNode head, int n) { ListNode dummpyNode = new ListNode(0); dummpyNode.next = head; ListNode pre = dummpyNode,cur = dummpyNode; for(int i = 0; cur != null;i++){ if(i > n) pre = pre.next; cur = cur.next; } pre.next = pre.next.next; return dummpyNode.next; } }

 19.有效的括号

给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。

有效字符串需满足:

左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。

注意空字符串可被认为是有效字符串。

使用栈
class
Solution { private Map<Character, Character> brackt = new HashMap<>(){{ put(')', '('); put(']', '['); put('}', '{'); }}; public boolean isValid(String s) { if(s == null) return false; Stack<Character> result = new Stack<Character>(); for(int i = 0; i < s.length(); i++){ char temp = s.charAt(i); if(brackt.containsKey(temp)){ char p = result.isEmpty()?'#':result.pop(); if(p != brackt.get(temp)) return false; }else{ result.push(temp); } } return result.isEmpty(); } }

 20.合并两个有序链表

将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 

示例:

输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4
暴力法依次读取两个链表的数据并比较
class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        ListNode result = new ListNode(0);
        ListNode cur = result;
        while(l1 != null || l2 != null){
            cur.next = new ListNode(0);//首先给result链表创建下一个节点,并初始化为0,让指针指向最后一个节点7
            cur = cur.next;
            if(l1 != null && l2 != null) {//这是需要进行比较的情况
                cur.val = l1.val > l2.val? l2.val: l1.val;
                if(l2.val > l1.val){
                    l1 = l1.next;
                }else{
                    l2 = l2.next;
                }
            }else if(l1 != null){//有一个为null的情况
                cur.val = l1.val;
                l1 = l1.next;
            }else{
                cur.val = l2.val;
                l2 = l2.next;
            }
        }
        return result.next;
    }
}

递归法:

我们可以如下递归地定义在两个链表里的 merge 操作(忽略边界情况,比如空链表等):

list1[0]+merge(list1[1:],list2)  list1[0]<list2[0]
list2[0]+merge(list1,list2[1:])  otherwise

也就是说,两个链表头部较小的一个与剩下元素的 merge 操作结果合并。

算法:我们直接将以上递归过程建模,首先考虑边界情况。
特殊的,如果 l1 或者 l2 一开始就是 null ,那么没有任何操作需要合并,所以我们只需要返回非空链表。否则,我们要判断 l1 和 l2 哪一个的头元素更小,然后递归地决定下一个添加到结果里的值。如果两个链表都是空的,那么过程终止,所以递归过程最终一定会终止。

class Solution {
    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;
        }
    }
}

官方迭代法:
class
Solution { public ListNode mergeTwoLists(ListNode l1, ListNode l2) { // maintain an unchanging reference to node ahead of the return node. ListNode prehead = new ListNode(-1); ListNode prev = prehead; while (l1 != null && l2 != null) { if (l1.val <= l2.val) { prev.next = l1; l1 = l1.next; } else { prev.next = l2; l2 = l2.next; } prev = prev.next; } // exactly one of l1 and l2 can be non-null at this point, so connect // the non-null list to the end of the merged list. prev.next = l1 == null ? l2 : l1; return prehead.next; } }

 21.删除排序数组中的重复项

给定一个排序数组,你需要在 原地 删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。

不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。

### 解题思路
定义一个指针pre记录不重复数据所在的位置,当pre处数据与后续数据i处不同时,直接将i处数据赋值到pre+1处。

### 代码

```java
class Solution {
    public int removeDuplicates(int[] nums) {
        int pre = 0;
        for(int i = 1; i < nums.length; i++){
            if(nums[pre] != nums[i]){
                pre++;
                nums[pre] = nums[i];                
            } 
        }
        return pre + 1;
    }
}

22.就地移除数组

给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。

不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

双指针法
class Solution {
    public int removeElement(int[] nums, int val) {
        int count = 0;
        int pre = 0;
        for(int i = 0; i < nums.length; i++){
            if(nums[i] != val){
                nums[pre++] = nums[i];
            }
        }
        return pre;
    }
}

23.实现strStr()

实现 strStr() 函数。

给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回  -1。

示例 1:

输入: haystack = "hello", needle = "ll"
输出: 2

示例 2:

输入: haystack = "aaaaa", needle = "bba"
输出: -1

说明:

当 needle 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。

对于本题而言,当 needle 是空字符串时我们应当返回 0 。这与C语言的 strstr() 以及 Java的 indexOf() 定义相符。

暴力法
class
Solution { public int strStr(String haystack, String needle) { if(haystack.length() < needle.length()) return -1; if(needle.equals("")) return 0; int temp = 0; for(int i = 0, j = 0; i < haystack.length(); i++){ temp = i; while(haystack.charAt(i) == needle.charAt(j)) { i++; j++; if(i == haystack.length() && j != needle.length()) return -1; if(j == needle.length()) return i - j; } i = temp; j = 0; } return -1; } }
字串逐一比较法:
class
Solution { public int strStr(String haystack, String needle) { int L = needle.length(), n = haystack.length(); for (int start = 0; start < n - L + 1; ++start) { if (haystack.substring(start, start + L).equals(needle)) { return start; } } return -1; } }
双指针法,当出现不同的字符时,马上回溯
class
Solution { public int strStr(String haystack, String needle) { int L = needle.length(), n = haystack.length(); if (L == 0) return 0; int pn = 0; while (pn < n - L + 1) { // find the position of the first needle character // in the haystack string while (pn < n - L + 1 && haystack.charAt(pn) != needle.charAt(0)) ++pn; // compute the max match string int currLen = 0, pL = 0; while (pL < L && pn < n && haystack.charAt(pn) == needle.charAt(pL)) { ++pn; ++pL; ++currLen; } // if the whole needle string is found, // return its start position if (currLen == L) return pn - L; // otherwise, backtrack pn = pn - currLen + 1; } return -1; } }

 

 

 

24.两数相除

给定两个整数,被除数 dividend 和除数 divisor。将两数相除,要求不使用乘法、除法和 mod 运算符。

返回被除数 dividend 除以除数 divisor 得到的商。

示例 1:

输入: dividend = 10, divisor = 3
输出: 3

示例 2:

输入: dividend = 7, divisor = -3
输出: -2

说明:

被除数和除数均为 32 位有符号整数。
除数不为 0。
假设我们的环境只能存储 32 位有符号整数,其数值范围是 [−231,  231 − 1]。本题中,如果除法结果溢出,则返回 231 − 1。

解题思路:

计算a/b(如果是负数,先转成正数,保证a和b都不是负数),基本思路是a一直减b,每减一次result+=1,直到0<= a <b(这里的a指的是上一次减完之后剩余的a)。

例如7/3,7 - 3 - 3 = 1, 0 <= 1 < 3,结果为2。

如果每次只减b,耗时太长,因此借助移位操作,每次减 b*2^n,同时result+=2^n。这里要满足b*2^n <= a(这里的a指的是上一次减完之后剩余的a)。

例如100/3,100 - 3*32 - 3 = 1,result = 32+1 = 33。

对于溢出的问题,考虑几种情况:

a = -2^31, b = -1, a/b = 2^31,直接返回 2^32-1
a = -2^31, b = -2^31, a/b = 1,直接返回1
a != -2^31, b = -2^31, a/b = 0,直接返回0
a = -2^31, b != -2^31,因为需要将a、b转成正数,而a转换后会溢出,因此先在a的基础上加 / 减b,使其绝对值减小,同时最终的result也需要再加上-1或1(也就是代码中的fix)。

class Solution {
    public int divide(int a, int b) {
        // a = -2^31, b = -1, a/b = 2^31
        if (a == Integer.MIN_VALUE && b == -1) return Integer.MAX_VALUE;
        // a = -2^31, b = -2^31, a/b = 1
        if (a == Integer.MIN_VALUE && b == Integer.MIN_VALUE) return 1;
        // a != -2^31, b = -2^31, a/b = 0
        if (b == Integer.MIN_VALUE) return 0;
        // a = -2^31, b != -2^31:  a <= a + abs(b), fix = b > 0 ? -1 : 1
        int fix = 0;
        if (a == Integer.MIN_VALUE) {
            if (b > 0) {
                a += b;
                fix = -1;
            } else {
                a -= b;
                fix = 1;
            }
        }
        boolean neg = false;
        if (a < 0) {
            a = -a;
            neg = !neg;
        }
        if (b < 0) {
            b = -b;
            neg = !neg;
        }
        int result = 0;
        while (a >= b) {
            int x = b;
            int r = 1;
            while (x <= (a>>1)) {
                x <<= 1;
                r <<= 1;
            }
            a -= x;
            result += r;
        }
        return (neg ? -result : result) + fix;
    }
}

25.Pow(x, n)

实现 pow(x, n) ,即计算 x 的 n 次幂函数。

示例 1:

输入: 2.00000, 10
输出: 1024.00000

示例 2:

输入: 2.10000, 3
输出: 9.26100

示例 3:

输入: 2.00000, -2
输出: 0.25000
解释: 2-2 = 1/22 = 1/4 = 0.25

说明:

-100.0 < x < 100.0
n 是 32 位有符号整数,其数值范围是 [−231, 231 − 1] 。
暴力法:超出时间限制
class
Solution { public double myPow(double x, int n) { if(x == 1 && n == 1) return x; if(n < 0){ n = -n; x = 1/x; } double result = 1.0; for(int i = 0; i < n; i++){ result *= x; } return result; } }

方法2:快速幂算法(递归)
直观想法:假定我们已经得到了 x ^ n的结果,我们如何得到 x ^ {2 * n}的结果?很明显,我们不需要将 x 再乘 n 次。使用公式 (x ^ n) ^ 2 = x ^ {2 * n},我们可以在一次计算内得到 x ^ {2 * n}的值。使用该优化方法,我们可以降低算法的时间复杂度。

算法:假定我们已经得到了 x ^ {n / 2}的结果,并且我们现在想得到 x ^ n的结果。我们令 A 是 x ^ {n / 2}的结果,我们可以根据 n 的奇偶性来分别讨论 x ^ n的值。如果 n 为偶数,我们可以用公式 (x ^ n) ^ 2 = x ^ {2 * n}来得到 x ^ n = A * A。如果 n 为奇数,那么 A * A = x ^ {n - 1}。直观上看,我们需要再乘一次 x ,即 x ^ n = A * A * x。

该方法可以很方便的使用递归实现。我们称这种方法为 "快速幂",因为我们只需最多O(logn) 次运算来得到 x ^ n。


class
Solution { private double fastPow(double x, long n) { if (n == 0) { return 1.0; } double half = fastPow(x, n / 2); if (n % 2 == 0) { return half * half; } else { return half * half * x; } } public double myPow(double x, int n) { long N = n; if (N < 0) { x = 1 / x; N = -N; } return fastPow(x, N); } }

方法 3:快速幂算法(循环)
直观想法:

使用公式 x ^ {a + b} = x ^ a * x ^ b,我们可以将 n 看做一系列正整数之和,n = ∑bi。如果我们可以很快得到 x ^ {b_i}的结果,计算 x ^ n的总时间将被降低。

算法:

我们可以使用 n 的二进制表示来更好的理解该问题。使 n 的二进制从最低位 (LSB) 到最高位 (MSB) 表示为b_1, b_2, ..., b_{length\_limit}。对于第 i 位为,如果 b_i = 1,意味着我们需要将结果累乘上 x ^ {2 ^ i}。

这似乎不能有效率上的提升,因为 \sum_i b_i * 2 ^ i = n∑bi∗2i=n 。但是使用上面提到的公式 (x ^ n) ^ 2 = x ^ {2 * n},我们可以进行改进。初始化 x ^ 1 = x,对于每一个 $ i > 1$ ,我们可以在一步操作中使用 x ^ {2 ^ {i - 1}}来得到 x ^ {2 ^ i}。由于 b_i的个数最多为O(logn) ,我们可以在 O(logn) 的时间内得到所有的 x ^ {2 ^ i}。在那之后,对于所有满足 b_i = 1的 i,我们可以用结果累乘 x ^ {2 ^ i}。这也只需要O(logn) 的时间。

class Solution {
    public double myPow(double x, int n) {
        long N = n;
        if (N < 0) {
            x = 1 / x;
            N = -N;
        }
        double ans = 1;
        double current_product = x;
        for (long i = N; i > 0; i /= 2) {
            if ((i % 2) == 1) {
                ans = ans * current_product;
            }
            current_product = current_product * current_product;
        }
        return ans;
    }
}

 

 

26.跳跃游戏

给定一个非负整数数组,你最初位于数组的第一个位置。

数组中的每个元素代表你在该位置可以跳跃的最大长度。

你的目标是使用最少的跳跃次数到达数组的最后一个位置。

示例:

输入: [2,3,1,1,4]
输出: 2

解释: 跳到最后一个位置的最小跳跃数是 2。

从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
说明:

假设你总是可以到达数组的最后一个位置。


顺瓜摸藤,我们知道最终要到达最后一个位置,然后我们找前一个位置,遍历数组,找到能到达它的位置,离它最远的就是要找的位置。
然后继续找上上个位置,最后到了第 0 个位置就结束了。
至于离它最远的位置,其实我们从左到右遍历数组,第一个满足的位置就是我们要找的。
class Solution {
  public int jump(int[] nums) {
     int position = nums.length - 1;
        int result = 0;
        while(position > 0){
            for(int i = 0; i < position; i++){
                if(nums[i] >= position - i){
                    position = i;
                    result++;
                    break;
                }
            }
        }
        return result;
    }
}
顺藤摸瓜,贪婪算法,我们每次在可跳范围内选择可以使得跳的更远的位置
class
Solution { public int jump(int[] nums) { int maxPosition = 0; int end = 0, steps = 0; for(int i = 0; i < nums.length - 1; i++){ maxPosition = Math.max(maxPosition, nums[i] + i); if(i == end) { end = maxPosition; steps++; } } return steps; } }
 

 27.k个一组翻转链表

给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。

k 是一个正整数,它的值小于或等于链表的长度。

如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

示例:

给你这个链表:1->2->3->4->5
当 k = 2 时,应当返回: 2->1->4->3->5
当 k = 3 时,应当返回: 3->2->1->4->5

说明:

你的算法只能使用常数的额外空间。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
每k个节点为一组,调换顺序。时间复杂度:N,空间复杂度为常数
class
Solution { public ListNode reverseKGroup(ListNode head, int k) { int len = 0; ListNode dummyNode = new ListNode(0); ListNode cur = head; dummyNode.next = head; ListNode tail = dummyNode, index = dummyNode; while(cur != null){ len++; cur = cur.next; } cur = dummyNode.next; for(int i = 0; i < len/k; i++){ tail = cur; cur = cur.next; for(int j = 1; j < k; j++){ ListNode next = cur.next; tail.next = next; cur.next = index.next; index.next = cur; cur = next; } index = tail; } if(cur != null){ index.next = cur; } return dummyNode.next; } }
尾插法
class Solution { public ListNode reverseKGroup(ListNode head, int k) { ListNode dummy = new ListNode(0); dummy.next = head; ListNode pre = dummy; ListNode tail = dummy; while (true) { int count = 0; while (tail != null && count != k) { count++; tail = tail.next; } if (tail == null) break; ListNode head1 = pre.next; while (pre.next != tail) { ListNode cur = pre.next; pre.next = cur.next; cur.next = tail.next; tail.next = cur; } pre = head1; tail = head1; } return dummy.next; } }
递归法
class Solution {
    public ListNode reverseKGroup(ListNode head, int k) {
        ListNode cur = head;
        int count = 0;
        while (cur != null && count != k) {
            cur = cur.next;
            count++;
        }
        if (count == k) {
            cur = reverseKGroup(cur, k);
            while (count != 0) {
                count--;
                ListNode tmp = head.next;
                head.next = cur;
                cur = head;
                head = tmp;
            }
            head = cur;
        }
        return head;
    }
}

 

 

28.下一个排列

实现获取下一个排列的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列。

如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。

必须原地修改,只允许使用额外常数空间。

以下是一些例子,输入位于左侧列,其相应输出位于右侧列。

 

一次遍历法
class
Solution { private void swap(int[] nums, int i, int j){ int temp = nums[i]; nums[i] = nums[j]; nums[j] = temp; } private void reverse(int[] nums, int start){ for(int i = start, j = nums.length - 1; i < j; i++, j--){ swap(nums, i, j); } } public void nextPermutation(int[] nums) { if(nums.length == 1) return; int i = nums.length - 2; while(i >= 0 && nums[i] >= nums[i + 1]) i--; if(i >= 0){ int j = nums.length - 1; while(j > 0 && nums[j] <= nums[i]) j--; swap(nums, i, j); } reverse(nums, i + 1); } }

 29.N皇后

回溯法
class
Solution { private List<List<String>> result = new ArrayList<>(); private int[] array; public List<List<String>> solveNQueens(int n) { array = new int[n]; check(0, n); return result; } private void check(int level, int n){ if (level == n){ parseList(n); return; } for(int i = 0; i < n; i++){ array[level] = i; if(judge(level)){ check(level + 1, n); } } } private boolean judge(int level){ for(int i = 0; i < level; i++){ if(array[i] == array[level] || Math.abs(level - i) == Math.abs(array[level] - array[i])){ return false; } } return true; } private void parseList(int n){ List<String> temp = new ArrayList<String>(); for(int i = 0; i < array.length; i++){ StringBuilder sb = new StringBuilder(); for(int j = 0; j < n; j++){ if(j == array[i]){ sb.append("Q"); }else{ sb.append("."); } } temp.add(sb.toString()); } result.add(temp); } }

 30.正则表达式匹配

回溯法
class
Solution { public boolean isMatch(String s, String p) { if(p.isEmpty()) return s.isEmpty(); boolean firstMatch = ((!s.isEmpty()) && (s.charAt(0) == p.charAt(0) || p.charAt(0) == '.')); if((p.length() >= 2) && (p.charAt(1) == '*')){ return (isMatch(s, p.substring(2)) || (firstMatch) && isMatch(s.substring(1), p)); }else{ return (firstMatch && isMatch(s.substring(1), p.substring(1))); } } }

31.通配符匹配

 

posted @ 2020-03-02 22:15  ifreewolf  阅读(319)  评论(0编辑  收藏  举报