算法 | 第1章 数组与字符串相关《程序员面试金典》

前言

本系列笔记主要记录笔者刷《程序员面试金典》算法的一些想法与经验总结,按专题分类,主要由两部分构成:经验值点和经典题目。其中重点放在经典题目上;


0. *经验总结

0.1 程序员面试金典 P76

  • 数组问题与字符串问题往往是相通的;
  • 散列表是一种通过将键(key)映射为值(value)从而实现快速查找的数据结构;
  • 可变长度数组ArrayList插入N个元素总计用时为O(N),平均每次插入操作用时O(1);
  • 拼接n个长度为x字符串,直接使用String的+用时O( n2);而StringBuilder可以避免此问题,它会创建一个足以容纳所有字符串的可变长度数组;
  • Java的字符串是不可变长度的,如果要改变长度,可以转成数组或使用StringBuilder;
  • 一般而言字符串的处理离不开集合,尤其是Map类型的集合;

0.2 ACSII码总结

ACSII编码 常用字符
48 0
57 9
65 A
90 Z
97 a
122 z

0.3 String的一些易忘的API

完整API请参考:个人总结的Java常用API手册汇总

构造方法:

  • String(byte[] bytes, int offset, int length):通过使用平台的默认字符集解码指定的 byte 子数组,构造一个新的 String。//把字节数组的一部分转换为字符串。

  • String(char[] value, int offset, int count):分配一个新的 String,它包含取自字符数组参数一个子数组的字符。//把字符数组的一部分转换为字符串。

    把字节/字符数组的一部分转换为字符串 offset:数组的开始索引 length:转换的字节个数 count:转换的字符个数

判断功能的方法:

  • boolean equalsIgnoreCase(String anotherString):将此字符串与指定对象进行比较,忽略大小写。

获取功能的方法:

  • String concat(String str):将指定的字符串连接到该字符串的末尾。
  • char charAt(int index):返回指定索引处的char值。
  • int indexOf(String str):返回指定子字符串第一次出现在该字符串内的索引。
  • String substring(int beginIndex):返回一个子字符串,从beginIndex开始截取字符串到字符串结尾。
  • String substring(int beginIndex, int endIndex):返回一个子字符串,从beginIndex到endIndex截取字符串。含beginIndex,不含endIndex。

转换功能的方法:

  • char[] toCharArray():将此字符串转换为新的字符数组。

  • byte[] getBytes():使用平台的默认字符集将该String编码转换为新的字节数组。

  • String replaceAll(String regex, String replacement):成功则返回替换的字符串,失败则返回原始字符串。其中regex为匹配此字符串的正则表达式;replacement为用来替换每个匹配项的字符串。

  • String replace(CharSequence target, CharSequencere placement):将与target匹配的字符串使用replacement字符串替换。

    CharSequence是一个接口,也是一种引用类型。作为参数类型,可以把String对象传递到方法中。

分割功能的方法:

  • String[] split(String regex):将此字符串按照给定的regex(规则)拆分为字符串数组。

    split方法的参数其实是一个“正则表达式”,如果按照英文句点“.”进行切分,必须写"\."。

将基本数据型态转换成String的static方法:

  • String.valueOf(Object o) : 将 Object 变量 o 转换成字符串,等于 obj.toString()。

    Object可以是: char、char[]、double、float等。

  • String.valueOf(char[] data, int offset, int count) : 将 char 数组 data 中 由 data[offset] 开始取 count 个元素 转换成字符串。

将String转换成基本数据型态的方法(Character除外):

  • static Object parseObject(String s):将字符串参数转换为对应的byte基本类型。

    Object可以是: byte、short、int、long、float、double、boolean等。

0.4 可以对字符串的字符先进行排序

  • Arrays.sort(int[] a, int fromIndex, int toIndex):对数组部分排序,也就是对数组a的下标从fromIndex到toIndex-1的元素排序;如果要从大到小需要实现Comparator接口;

  • Arrays.sort(obj):可以对obj对象排序;

    obj对象一般是数组,包括各种数组;

0.5 使用Map来统计每个字符出现次数(记住即可)

private Map getMap(String str) {
    Map map = new HashMap<>();
    char[] chars = str.toCharArray();
    for (char aChar : chars) {
        map.put(aChar, map.getOrDefault(aChar, 0) + 1);
    }
    return map;
}

0.6 遍历Map的四种方式

主要就两种方法:

  • 第一种是通过keySet()方法获得key,再通过map.get(key)方法,得到值;
  • 第二种是先用entrySet()方法转为Set类型,其中set的每一个元素值就是map的一个键值对,即Map.Entry
//方法一:普通的foreach循环,使用keySet()方法,遍历key
for(Integer key : map.keySet()){
    System.out.println("key = " + key);
    System.out.println("Value = " + map.get(key));
}

//方法二:把所有的键值对装入迭代器中,然后遍历迭代器
Iterator> it = map.entrySet().iterator();
    while(it.hasNext()){
        Map.Entry entry=it.next();
        System.out.println("key = "+entry.getKey());
        System.out.println("Value = "+entry.getValue());
}

//方法三:分别得到key和value
for(Integer obj : map.keySet()){
    System.out.println("key = " + obj);
}
for(String obj : map.values()){
    System.out.println("value = " + obj);
}

//方法四,entrySet()方法
Set> entries=map.entrySet();
for (Map.Entry entry:entries){
    System.out.println("key = " + entry.getKey());
    System.out.println("value = " + entry.getValue());
}

0.7 获取字符串字符的两种方式

  • 遍历字符串时char[] c = S.toCharArray()的索引效率更高;
  • 通过s.charAt(i)方法索引多了方法栈和越界检查的消耗;


1. 判断字符是否唯一 [easy]

判断字符是否唯一

1.1 考虑点

  • 字符是否为26个英文字母;
    • 如果是,可以先判断如果字符长度>26, 直接返回False;
  • 是否为ASCII字符集;
    • 如果是ASCII,考虑边界检查,判断是否超过ASCII字符集范围(0~128,扩展为255);
    • 如果是unicode,没有字符范围,需要扩大存储空间,可以先排序再判断;
  • 是否允许修改字符串;
    • 如果允许,可以先在O(nlog(n))的时间复杂度内对字符串排序,然后线性检查有无相邻字符完全相同;
  • 大小写算不算相同;

1.2 解法

1.2.1 使用HashMap统计字符次数

public boolean isUnique(String astr) {
    Map map = new HashMap<>();
    for(int i = 0; i < astr.length(); i++ ){
        if(map.containsKey(astr.charAt(i))){
            return false;
        }
        map.put(astr.charAt(i), i);
    }
    return true;
}
  • 执行时间:100.00%;内存消耗:69.76%;

1.2.2 利用boolean数组(优)

  • 需要知道需要判断的字符范围;
public boolean isUnique(String astr) {
    if( astr.length() > 128 ){
        return true;
    }
    boolean[] arr = new boolean[128];
    for (int i = 0; i < astr.length(); i++) {
        int c = (int)astr.charAt(i);
        if ( arr[c] ){
            return false;
        }
        arr[c] = true;
    }
    return true;
}
  • 执行时间:100.00%;内存消耗:69.76%;
  • 时间复杂度:O(n);或者O(1),因为for循环迭代不会超过128次;
  • 空间复杂度:O(1)

1.2.3 位运算符(优)

public boolean isUnique(String astr) {
    int mark = 0;
    for (int i = 0; i < astr.length() ; i++) {
        //移动距离
       int move = (int)astr.charAt(i) - 'a';
       if ((mark & (1<< move) )!=0){
           return false;
       }else {
           mark |=(1<
  • 执行时间:100.00%;内存消耗:63.69%;
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

1.2.4 双循环

  • 双循环开销O(n2),不推荐;
public boolean isUnique(String astr) {
    if( astr == null || "".equals(astr)){
        return true;
    }
    int left = 0;
    int rigth = astr.length();
    for(int i = 0; i < astr.length(); i++){
        for(int j = i+1; j < astr.length(); j++){
            if(astr.charAt(i) == astr.charAt(j)){
                return false;
            }
        }
    }
    return true;
}
  • 执行时间:100.00%;内存消耗:65.16%;
  • 时间复杂度:O(n2)
  • 空间复杂度:O(1)


2. 判定是否互为字符重排 [easy]

判定是否互为字符重排

2.1 考虑点

  • 询问面试官是否可以更改s1和s2的值;
  • 问清楚变位词是否区分大小写(如Dog和dog);
  • 是否考虑空白字符;

2.2 解法

2.2.1 先排序后比较

public boolean CheckPermutation(String s1, String s2) {
    // 将字符串转换成字符数组
    char[] c1 = s1.toCharArray();
    char[] c2 = s2.toCharArray();
    // 对字符数组进行排序
    Arrays.sort(c1);
    Arrays.sort(c2);
    // 再将字符数组转换成字符串,比较是否相等
    return new String(c1).equals(new String(c2));
}
  • 执行时间:100.00%;内存消耗:35.67%;
  • JDK中Arrays.sort排序:JDK7以后,对应基本变量数组采用变异的快速排序方法DualPivotQuicksort,对于对象数组比较由原来的mergeSort改为ComparableTimSort方法,TimSort当数组大小小于32时,采用二分插入排序算法;当大于32时,采用基于块-区run的归并排序。所以TimSort是一种二分插入排序和归并排序的变种算法。对对象进行排序,没有采用快速排序,是因为快速排序是不稳定的,而Timsort是稳定的。与其他合并排序一样,Timesrot是稳定的排序算法,最坏时间复杂度是O(nlogn)。在最坏情况下,Timsort算法需要的临时空间是n/2,在最好情况下,它只需要一个很小的临时存储空间;
  • 方法不算最优,但胜在清晰、简单易懂,不失为一种上佳之选;

2.2.2 HashMap计数(优)

public boolean CheckPermutation(String s1, String s2) {
    if (s1.length() != s2.length()) {
        return false;
    }
    char[] s1Chars = s1.toCharArray();
    Map s1Map = getMap(s1);
    Map s2Map = getMap(s2);
    for (char s1Char : s1Chars) {
        if (!s2Map.containsKey(s1Char) || s2Map.get(s1Char) != s1Map.get(s1Char)) {
            return false;
        }
    }
    return true;
}
// 统计指定字符串str中各字符的出现次数,并以Map的形式返回
private Map getMap(String str) {
    Map map = new HashMap<>();
    char[] chars = str.toCharArray();
    for (char aChar : chars) {
        map.put(aChar, map.getOrDefault(aChar, 0) + 1);
    }
    return map;
}
  • 执行时间:100.00%;内存消耗:62.49%;
  • 时间复杂度:O(n);
  • 空间复杂度:O(1);

2.2.3 桶计数法

用数组来统计每个字符的出现次数;

public boolean CheckPermutation(String s1, String s2) {
    if (s1.length() != s2.length()) {
        return false;
    }
    int[] c1 = count(s1);
    int[] c2 = count(s2);
    for (int i = 0; i < c1.length; i++) {
        if (c1[i] != c2[i]) {
            return false;
        }
    }
    return true;
}
private int[] count(String str) {
    int[] c = new int[26];
    char[] chars = str.toCharArray();
    for (char aChar : chars) {
        c[aChar - 'a']++;
    }
    return c;
}
  • 执行时间:100.00%;内存消耗:30.77%;
  • 时间复杂度:O(n);
  • 空间复杂度:O(1);

2.2.4 一个偷巧的方法

public boolean CheckPermutation(String s1, String s2) {
    int sum1 = 0;
    int sum2 = 0;
    if(s1.length() != s2.length()) {
        return false;
    }
    for(int i = 0; i < s1.length(); i++){
        sum1 += s1.charAt(i);
        sum2 += s2.charAt(i);
    }
    return (sum1 == sum2); 
}
  • 不推荐
  • 时间复杂度:O(n);
  • 空间复杂度:O(1);
  • 一个骗测试用例的方法,先累加字符串各个字符的ASCII码,再进行比较。但不能区分“bbb" 和“abc”;
  • 但通过ASCII的解题思路还是值得思考一番的;

3. URL化 [easy]

URL化

3.1 考虑点

  • Java的String是不可变的,可以先思考给出的字符串是否“够长”,如果够长,从后面开始处理可能不会覆盖原有数据;

3.2 解法

3.2.1 一行流法:使用String类的API

public String replaceSpaces(String S, int length) {
    return S.substring(0, length).replaceAll(" ", "%20");
}
  • 执行时间:12.07%;内存消耗:30.13%;
  • String substring(int beginIndex, int endIndex):返回一个子字符串,从beginIndex到endIndex截取字符串。含beginIndex,不含endIndex。
  • String replaceAll(String regex, String replacement):成功则返回替换的字符串,失败则返回原始字符串。其中regex为匹配此字符串的正则表达式;replacement为用来替换每个匹配项的字符串。

3.2.2 可变长度字符串StringBuilder

public String replaceSpaces(String S, int length) {
    if(S ==  null || length < 0){
        return null;
    }
    char[] c = S.toCharArray();
    StringBuilder sb = new StringBuilder();
    for(int i = 0; i < c.length; i++){
        if( c[i] == ' ' && length == 0){
            return sb.toString();
        }
        if(c[i] == ' '){
            sb.append("%20");
        } else {
            sb.append(c[i]);
        }
        length--;
    }
    return sb.toString();
}
  • 执行时间:46.27%;内存消耗:8.08%;

3.2.3 倒序计算(优)

public String replaceSpaces(String S, int length) {
    //先把字符串转化为字符数组
    char[] chars = S.toCharArray();
    int index = chars.length - 1;
    for (int i = length - 1; i >= 0; i--) {
        //如果遇到空格就把他转化为"%20"
        if (chars[i] == ' ') {
            chars[index--] = '0';
            chars[index--] = '2';
            chars[index--] = '%';
        } else {
            chars[index--] = chars[i];
        }
    }
    return new String(chars, index + 1, chars.length - index - 1);
}
  • 执行时间:99.70%;内存消耗:51.01%;
  • 该方法的特点是不用开辟额外空间;
  • 上乘做法,因为字符串尾部有额外的缓冲,可以直接修改,不必担心会覆盖写原有数据;
  • 如果题目没有显示描述“有足够空间”,可以先遍历空格数量n,再对字符串进行3n扩展以获得足够空间;

4. 回文排列 [easy]

回文排列
  • 回文串:从正、反两个方向读都一致的字符串;
  • 即:偶数长度的字符串所有字符必须出现偶数次;奇数长度的字符串必须只有一个字符出现奇数次;

4.1 考虑点

  • 思考的地方在字符串字符出现次数的奇偶关系;
  • 不能构造出所有可能排列后比较判断,因为时间复杂度是阶乘级别的;

4.2 解法

4.2.1 散列表统计法

public boolean canPermutePalindrome(String s) {
    if(s == null){
        return false;
    }
    Map map = getMap(s);
    int single = 0;
    for( Map.Entry entry : map.entrySet()){
        if( entry.getValue() % 2 == 1){
            single++;
        }
    }
    if((s.length() % 2 == 0) && (single == 0)){
        return true;
    }
    if((s.length() % 2 == 1) && (single == 1)){
        return true;
    }
    return false;
}
// 统计指定字符串str中各字符的出现次数,并以Map的形式返回
private Map getMap(String str) {
    Map map = new HashMap<>();
    char[] chars = str.toCharArray();
    for (char aChar : chars) {
        map.put(aChar, map.getOrDefault(aChar, 0) + 1);
    }
    return map;
}
  • 执行时间:45.13%;内存消耗:51.07%;
  • 时间复杂度:O(n);

4.2.2 利用HashSet的唯一性原理(优)

public boolean canPermutePalindrome(String s) {
    Set set = new HashSet<>();
    for (char ch : s.toCharArray()) {
        //set的add方法如果返回false,表示已经有了,就删除
        if (!set.add(ch)) {
            set.remove(ch);
        }
    }
    return set.size() <= 1;
}
  • 执行时间:100.00%;内存消耗:78.48%;

HashSet的规则

  • 新添加到HashSet集合的元素都会与集合中已有的元素一一比较;
  • 首先比较哈希值(每个元素都会调用hashCode()产生一个哈希值);
    • 如果新添加的元素与集合中已有的元素的哈希值都不同,新添加的元素存入集合;
    • 如果新添加的元素与集合中已有的某个元素哈希值相同,此时还需要调用equals()方法比较;
      • 如果equals()方法返回true,说明新添加的元素与集合中已有的某个元素的属性值相同,那么新添加的元素不存入集合;
      • 如果equals()方法返回false,说明新添加的元素与集合中已有的元素的属性值都不同,,那么新添加的元素存入集合;

set.size() <= 1:

  • 最后判断set的长度是否小于等于1,如果等于1说明只有一个字符的个数是奇数,其他的都是偶数。如果等于0说明每个字符都是偶数,否则不可能构成回文字符串;

4.2.3 count计数法(优)

public boolean canPermutePalindrome(String s) {
    int[] map = new int[128];
    int count = 0;
    for (char ch : s.toCharArray()) {
        if ((map[ch]++ & 1) == 1) {
            count--;
        } else {
            count++;
        }
    }
    return count <= 1;
}
  • 执行时间:100.00%;内存消耗:76.73%;

  • 结果其实与字符的奇偶性无关,遇到不同count++,遇到相同count--,如果满足回文,count的结果只有可能是0或1;

  • 有点类似于HashSet的唯一性;

4.2.4 位运算(优)

public boolean canPermutePalindrome(String s) {
    long highBitmap = 0;
    long lowBitmap = 0;
    for (char ch : s.toCharArray()) {
        if (ch >= 64) {
            highBitmap ^= 1L << ch - 64;
        } else {
            lowBitmap ^= 1L << ch;
        }
    }
    return Long.bitCount(highBitmap) + Long.bitCount(lowBitmap) <= 1;
}
  • 执行时间:100.00%;内存消耗:75.90%;
  • 在位运算中有类似奇偶性质;
  • 在128位的字符中,如果是用int类型,需要4位,但如果使用long类型,只需要两位就行了。一个记录0-63,一个记录64-127。每一位对应一个字符,如果当前位置是1,表示有字符了,那么加上当前字符就是2个,我们把它变为0。如果当前位置没有字符,我们就把当前位置变为1,表示有一个字符。最后在判断这两个long类型中1的个数,如果大于1个就不能构成回文排列。


5. 一次编辑 [medium]

一次编辑

5.1 考虑点

  • 对于操作“一次”的题目,可以分为找到前和找到后两种方向处理;

5.2 解法

5.2.1 count计数找不同法

public boolean oneEditAway(String first, String second) {
    if( first == null && second == null){
        return true;
    }
    if( first == null ){
        return false;
    }
    if( second == null ){
        return false;
    }
    if(first.length() == second.length()){
    //判断替换
        int count = 0;
        for(int i = 0; i < first.length(); i++){
            if( first.charAt(i) != second.charAt(i) ){
                count++;
            }
        }
        return count <= 1;
    } else {
        //判断增删
        //替换找最长
        String longger;
        String shortter;
        if( first.length() > second.length() ){
            longger = first;
            shortter = second;
        } else {
            longger = second;
            shortter = first;
        }
        if(longger.length() == 1 && shortter.length() == 0){
            return true;
        }
        //用count记录不同次数
        int count = 0;
        int j = 0;
        for( int i = 0; i < shortter.length() ; i++, j++){
            if( longger.charAt(j) != shortter.charAt(i) ){
                count++;
                i--;
            }
            if( count > 1){
                return false;
            }
        }
        //判断长字符串最后一个字符是否不同
        if( j  == longger.length() - 1){
            count++;
            if( count > 1){
                return false;
            }
        }
        return j == longger.length()-1 || j == longger.length();
    }
}
  • 执行时间:53.77%;内存消耗:26.60%;
  • 其中“替换找最长”简写成:if(second.length()>first.length) return oneEditAway(second, first)
  • 时间复杂度:O(min(n,m)),n和m分别为两个字符串的长度;

5.2.2 剩余字符串比较法(优)

public boolean oneEditAway(String first, String second) {
    if (first == null || second == null) return false;
    int len1 = first.length();
    int len2 = second.length();
    if (Math.abs(len1 - len2) > 1) return false;
    // 保持第一个比第二个长
    if (len2 > len1) return oneEditAway(second, first);

    for (int i = 0; i < len2; i++){
        if (first.charAt(i) != second.charAt(i)){
            //如果是长度相同字符串,那就比较下一个,如果长度不一样,那就从该字符开始进行比较。
            return first.substring(i + 1).equals(second.substring(len1 == len2 ? i + 1 : i));
        }
    }
    return true;
}
  • 执行时间:53.77%;内存消耗:17.11%;
  • 找到第一个不同处后,对后面字符串进行equals判断即可;
  • 思考点:对于只需要找出“一次”的题目,可以分为找到前和找到后两种方向处理;

5.2.3 动态规划

public boolean oneEditAway(String word1, String word2) {
    int length1 = word1.length();
    int length2 = word2.length();
    int dp[][] = new int[length1 + 1][length2 + 1];
    for (int i = 0; i <= length1; i++) {
        dp[i][0] = i;//边界条件,相当于word1的删除操作
    }
    for (int i = 0; i <= length2; i++) {
        dp[0][i] = i;//边界条件,相当于word1的添加操作
    }
    for (int i = 1; i <= word1.length(); i++) {
        for (int j = 1; j <= length2; j++) {//下面是上面分析的递推公式
            if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                dp[i][j] = dp[i - 1][j - 1];
            } else {
                dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;
            }
        }
    }
    return dp[length1][length2] <= 1;
}
  • 执行时间:10.57%;内存消耗:20.57%;
  • 杀鸡用牛刀,不推荐;


6. 字符串压缩 [easy]

在这里插入图片描述

6.1 考虑点

  • 如果先检查原字符串与压缩后字符串长度,在没有很多字符重复下可以避免构造不会被使用的字符串,代价是需要增加近乎重复的代码;

6.2 解法

6.2.1 逐一遍历法(优)

public String compressString(String S) {
    if( S == null || "".equals(S)){
        return S;
    }
    char[] chs = S.toCharArray();
    char cache = chs[0];
    StringBuilder sb = new StringBuilder();
    sb.append(chs[0]);
    int count = 0;
    for(int i = 0; i < S.length(); i++){
        if( cache != chs[i] ){
            sb.append(count);
            sb.append(chs[i]);
            cache = chs[i];
            count = 1;
        } else {
            count++;
        }
        if( i == S.length() -1 ){
            sb.append(count);
        }
    }
    String result = sb.toString();
    return result.length() < S.length() ? result : S;
}
  • 执行时间:76.02%;内存消耗:50.21%;
  • 时间复杂度:O(n),其中 n 为字符串的长度,即遍历一次字符串的复杂度;
  • 空间复杂度:O(1),只需要常数空间(不包括存储答案 ans 的空间)存储变量;


7. 旋转矩阵 [medium]

旋转矩阵

7.1 考虑点

  • 询问面试官是否可以开辟额外空间,如果可以,可以使用第一种辅助数组的解法;如果不能,只能原地旋转或翻转法;
  • 任何算法都需要O(n2)时间复杂度;

7.2 解法

7.2.1 使用辅助数组逐层复制法(优)

public void rotate(int[][] matrix) {
    int line = matrix.length;
    int row = matrix[0].length;
    if(line == 0){
        return;
    }
    int[][] result = new int[row][line];
    for( int i = 0; i < line; i++){
        for( int j = 0; j < row; j++){
            result[j][line-1-i] = matrix[i][j];
        }
    }
    for(int i = 0; i < row; i++){
        for( int j = 0; j < line; j++){
            matrix[i][j] = result[i][j];
        }
    }
}
  • 执行时间:100.00%;内存消耗:30.87%;
  • 时间复杂度:O(n2);
  • 需要使用额外空间;

7.2.2 原地旋转法

public void rotate(int[][] matrix) {
    int n = matrix.length;
    for (int i = 0; i < n / 2; ++i) {
        for (int j = 0; j < (n + 1) / 2; ++j) {
            int temp = matrix[i][j];
            matrix[i][j] = matrix[n - j - 1][i];
            matrix[n - j - 1][i] = matrix[n - i - 1][n - j - 1];
            matrix[n - i - 1][n - j - 1] = matrix[j][n - i - 1];
            matrix[j][n - i - 1] = temp;
        }
    }
}
  • 执行时间:100.00%;内存消耗:61.77%;
  • 时间复杂度:O(N2),其中 N 是 matrix 的边长。我们需要枚举的子矩阵大小 O(⌊n/2⌋×⌊(n+1)/2⌋)=O(N2)。
  • 空间复杂度:O(1)。为原地旋转。
  • 对数学水平要求较高;
  • 不使用额外内存空间;

7.2.3 翻转法(优)

public void rotate(int[][] matrix) {
    int n = matrix.length;
    // 水平翻转
    for (int i = 0; i < n / 2; ++i) {
        for (int j = 0; j < n; ++j) {
            int temp = matrix[i][j];
            matrix[i][j] = matrix[n - i - 1][j];
            matrix[n - i - 1][j] = temp;
        }
    }
    // 主对角线翻转
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < i; ++j) {
            int temp = matrix[i][j];
            matrix[i][j] = matrix[j][i];
            matrix[j][i] = temp;
        }
    }
}
  • 执行时间:100.00%;内存消耗:50.10%;
  • 时间复杂度:O(N2),其中 N 是 matrix 的边长。对于每一次翻转操作,我们都需要枚举矩阵中一半的元素。
  • 空间复杂度:O(1)。为原地翻转得到的原地旋转。
  • 不使用额外内存空间;


8. 零矩阵 [medium]

零矩阵

8.1 考虑点

  • 对空间复杂度的思考,需要注意陷阱,发现0直接将整行整列置0最后全是0;

8.2 解法

8.2.1 标记数组法

public void setZeroes(int[][] matrix) {
    int line = matrix.length;
    int row = matrix[0].length;
    if(line == 0){
        return;
    }
    boolean[][] isZero = new boolean[line][row];
    for(int i = 0; i < line; i++){
        for(int j =0; j < row; j++){
            //没有被标记
            if( !isZero[i][j] ){
                //处理0情况并标记
                if(matrix[i][j] == 0){
                    for(int k = 0; k < row; k++){
                        if(matrix[i][k] != 0){
                            matrix[i][k] = 0;
                            isZero[i][k] = true;
                        }
                    }
                    for(int k = 0; k < line; k++){
                        if(matrix[k][j] != 0 ){
                            matrix[k][j] = 0;
                            isZero[k][j] = true;
                        }
                    }
                }
                
            }
        }
    }
}
  • 执行时间:98.04%;内存消耗:58.15%;
  • 时间复杂度:O(mn)O(mn),其中 mm 是矩阵的行数,nn 是矩阵的列数。我们至多只需要遍历该矩阵两次;
  • 空间复杂度:O(mn),其中 m 是矩阵的行数,n 是矩阵的列数。新创建了个mn的二维数组;

8.2.2 使用两个标记变量法(优)

public void setZeroes(int[][] matrix) {
    int m = matrix.length, n = matrix[0].length;
    boolean flagCol0 = false, flagRow0 = false;
    //检查第一列是否有0
    for (int i = 0; i < m; i++) {
        if (matrix[i][0] == 0) {
            flagCol0 = true;
        }
    }
    //检查第二列是否有0
    for (int j = 0; j < n; j++) {
        if (matrix[0][j] == 0) {
            flagRow0 = true;
        }
    }
    //检查其他元素是否有0,有0则根据下标在第一行与第一列置0
    for (int i = 1; i < m; i++) {
        for (int j = 1; j < n; j++) {
            if (matrix[i][j] == 0) {
                matrix[i][0] = matrix[0][j] = 0;
            }
        }
    }
    //根据第一行与第一列的值置空
    for (int i = 1; i < m; i++) {
        for (int j = 1; j < n; j++) {
            if (matrix[i][0] == 0 || matrix[0][j] == 0) {
                matrix[i][j] = 0;
            }
        }
    }
    //置空第一列
    if (flagCol0) {
        for (int i = 0; i < m; i++) {
            matrix[i][0] = 0;
        }
    }
    //置空第一行
    if (flagRow0) {
        for (int j = 0; j < n; j++) {
            matrix[0][j] = 0;
        }
    }
}
  • 执行时间:98.04%;内存消耗:69.77%;
  • 时间复杂度:O(mn):其中 m 是矩阵的行数,n 是矩阵的列数。我们至多只需要遍历该矩阵两次;
  • 空间复杂度:O(1):我们只需要常数空间存储若干变量;
  • 用矩阵的第一行和第一列代替方法一中的两个标记数组,以达到 O(1) 的额外空间。但这样会导致原数组的第一行和第一列被修改,无法记录它们是否原本包含 0。因此我们需要额外使用两个标记变量分别记录第一行和第一列是否原本包含 0。

8.2.3 一次遍历法

public void setZeroes(int[][] matrix) {
    int m = matrix.length, n = matrix[0].length;
    boolean flagCol0 = false;
    for (int i = 0; i < m; i++) {
        if (matrix[i][0] == 0) {
            flagCol0 = true;
        }
        for (int j = 1; j < n; j++) {
            if (matrix[i][j] == 0) {
                matrix[i][0] = matrix[0][j] = 0;
            }
        }
    }
    for (int i = m - 1; i >= 0; i--) {
        for (int j = 1; j < n; j++) {
            if (matrix[i][0] == 0 || matrix[0][j] == 0) {
                matrix[i][j] = 0;
            }
        }
        if (flagCol0) {
            matrix[i][0] = 0;
        }
    }
}
  • 执行时间:98.04%;内存消耗:26.56%;
  • 时间复杂度:O(mn),其中 m 是矩阵的行数,n 是矩阵的列数。我们至多只需要遍历该矩阵两次;
  • 空间复杂度:O(1)。我们只需要常数空间存储若干变量;
  • 只使用一个标记变量记录第一列是否原本存在 0。这样,第一列的第一个元素即可以标记第一行是否出现 0。但为了防止每一列的第一个元素被提前更新,我们需要从最后一行开始,倒序地处理矩阵元素。


9. 字符串轮转 [easy]

字符串轮转

9.1 考虑点

  • 询问面试官是否为一次旋转还是多次旋转;
    • 如果是一次,先排序后判断和使用Map统计数量的方法是不符合要求的;
    • 如果是多次,寻找父段法是不行的;

9.2 解法

9.2.1 先排序后比较法

public boolean isFlipedString(String s1, String s2) {
    if(s1.length() != s2.length()){
        return false;
    }
    char[] c1 = s1.toCharArray();
    char[] c2 = s2.toCharArray();
    int[] count = new int[128];
    for( int i = 0; i < s1.length(); i++){
        count[c1[i]]++;
        count[c2[i]]--;
    }
    for( int i = 0; i < 128; i++){
        if(count[i] != 0){
            return false;
        }
    }
    return true;
}
  • 执行时间:24.35%;内存消耗:33.25%;

9.2.2 寻找父段法(优)

public boolean isFlipedString(String s1, String s2) {
        if(s1.length() != s2.length()) {
        return false;
    }
    String s = s2 + s2;
    return s.contains(s1);
}
  • 执行时间:100.00%;内存消耗:59.46%;

最后

你可能感兴趣的:(算法 | 第1章 数组与字符串相关《程序员面试金典》)