剑指offer题解(Java实现)—— 面试题5:替换空格

文章目录

  • 前言
  • 题目描述
  • 解题思路
  • 相关题目
    • 题目描述
    • 解题思路
  • 总结
  • 后记

前言

本系列的文章为笔者学习《剑指offer第二版》后的笔记整理以及Java实现,解法不保证与书上一致。

另外,我最近在系统整理一些 Java 后台方面的面试题和参考答案,有找工作需求的童鞋,欢迎关注我的 Github 仓库,如果觉得不错可以点个 star 关注 :

  • 1、awesome-java-interview
  • 2、awesome-java-notes

题目描述

请实现一个函数,将一个字符串中的空格替换成“%20”。例如,当字符串为“We Are Happy.”则经过替换之后的字符串为“We%20Are%20Happy.”。

解题思路

通常,按照习惯性思维,我们总习惯从左到右处理字符串。当遇到第一个空格时,将其换成“%20”并将其后的所有字符都往后移动两个位置;当遇到第二个空格时,将其替换成“%20”并将其后的所有字符都往后移动两个位置…正因为这样,导致很多字符移动了很多次。这种情况下,假设字符串长度为 n,对于每个空格而言都需要移动其后面 O(n) 个字符,因此对于含有 O(n) 个空格的字符串而言,总的时间复杂度为 O(n^2)。

这种从前往后遍历的方式需要那么高的时间复杂度,那么能不能减少一些元素移动的次数呢?我们不妨反过来从后往前试试。

  • 先遍历一遍字符串,统计空格字符的数目;
  • 每替换一个空格,长度增加 2(相当于将一个空格字符变成了%20的三个字符),因此替换后字符串的总长度为原来的长度加上空格的数目乘以 2;
  • 设置两个指针:indexOfOldindexOfNew,分别指向原始字符串的末尾和替换之后的字符串的末尾。接下来,不断地将indexOfOld处的字符移动到indexOfNew,同时向前(左)移动两个指针;如果indexOfOld处为空格字符,则indexOfOld向前移动一格,而indexOfNew指针前插入字符串“%20”,并向前移动 3 格。

从后往前的解法中,由于所有的字符都只复制(移动)一次,因此这个算法的时间复杂度为 O(n),比从前往后遍历的思路要块很多。

/**
 * 请实现一个函数,将一个字符串中的空格替换成“%20”。例如,当字符串为
 * “We Are Happy.”则经过替换之后的字符串为“We%20Are%20Happy.”。
 *
 * @author Rotor
 * @since 2019/9/29 22:53
 */
public class ReplaceBlank {
    public void replaceBlank(StringBuffer str) {
        if (str == null) {
            return;
        }
        // 统计空格数目
        int oldStrLength = 0;
        int numOfBlank = 0;
        for (int i = 0; i < str.length(); i++) {
            if (str.charAt(i) == ' ') {
                numOfBlank++;
            }
        }
        // 原字符串的尾指针
        int indexOfOld = str.length() - 1;
        // 设置新的长度
        str.setLength(str.length() + 2 * numOfBlank) ;
        // 新的字符串的末尾指针
        int indexOfNew = str.length() - 1;
        // 当新的字符串的末尾指针和原字符串的末尾指针指向同一位置时,表明所有空格都已替换完毕
        while (indexOfOld >= 0 && indexOfNew > indexOfOld) {
            if (str.charAt(indexOfOld) == ' ') {
                str.setCharAt(indexOfNew--, '0');
                str.setCharAt(indexOfNew--, '2');
                str.setCharAt(indexOfNew--, '%');
            } else {
                str.setCharAt(indexOfNew--, str.charAt(indexOfOld));
            }
            indexOfOld--;
        }
    }
}

相关题目

题目描述

有两个排序的数组 A1 和 A2,内存在 A1 的末尾有足够多的空余空间容纳 A2。请实现一个函数,把 A2 中的所有数字插入 A1 中,并且所有的数字是排序的。

解题思路

首先很容易想到对 A1 从前往后复制数字,将 A1 和 A2 中的元素从前往后比较,并插入到 A1 中,然后往后移动剩余数字,但这种方式会重复多次移动一个数字,时间效率很低。

我们可以换个方式,从后往前遍历。因为从题目中可知,A1、A2 是有序的,并且 A1 中有足够的空余空间容纳 A2 中的元素。所以可以这么做:

  • 设置两个指针分别指向 A1 和 A2 的最后一个元素,往后从后往前依次遍历并比较,将值较大的元素移动到 A1 的末尾;
  • 继续向前移动值较大的那个元素的指针,并不断重复上述过程;
  • 当某个数组中的值取完了,则直接从另一个数组继续取元素。
package com.offers.chapter2.question05;

import java.util.Arrays;

/**
 * 相关题目:有两个排序的数组 A1 和 A2,内存在 A1 的末尾有足够多的空余空间容纳 A2。
 * 请实现一个函数,把 A2 中的所有数字插入 A1 中,并且所有的数字是排序的。
 *
 * @author Rotor
 * @since 2019/9/30 9:46
 */
public class Merge2SortedArray {
    public static void merge(Integer[] arrayA, Integer[] arrayB) {
        // 统计A数组中有值元素的个数
        int lengthOfA = 0;
        for (int i = 0; i < arrayA.length; i++) {
            if (arrayA[i] != null) {
                lengthOfA++;
            }
        }
        // 指向数组A中不为空的最后一个元素的尾指针
        int pA = lengthOfA - 1;
        // 指向数组B不为空的最后一个元素的尾指针
        int pB = arrayB.length - 1;
        // 指向合并后数组不为空的最后一个元素的尾指针
        int pMerge = lengthOfA + arrayB.length - 1;
        while (pMerge >= 0) {
            // A 数组中元素取完了,接下来直接取 B 数组中的
            if (pA < 0) {
                arrayA[pMerge--] = arrayB[pB--];
                // B 数组中元素取完了,接下来直接取 A 数组中的
            } else if (pB < 0) {
                arrayA[pMerge--] = arrayA[pA--];
                // A 中元素更大,取A 中的
            } else if (arrayA[pA] > arrayB[pB]) {
                arrayA[pMerge--] = arrayA[pA--];
                // B 中元素更大,取 B 中的
            } else {
                arrayA[pMerge--] = arrayB[pB--];
            }
        }
    }

    public static void main(String[] args) {
        Integer[] arrayA = new Integer[10];
        Integer[] arrayB = {1, 3, 5, 7, 9};
        // 初始化 A 为 {2, 4, 6, 8, 10}
        for (int i = 0; i < 5; i++) {
            arrayA[i] = i * 2 + 2;
        }
        merge(arrayA, arrayB);
        System.out.println("合并后的数组为:" + Arrays.toString(arrayA));
    }
}

总结

  • 在合并两个数组(包括字符串)时,如果从前往后复制每个数字(或字符串)则需要重复移动数字(或字符)多次,那么我们可以考虑从后往前复制,这样就能减少移动的次数,从而提高效率
  • 从前往后遍历的解法会将某些字符移动多次,而从后往前遍历的解法则只会每个字符都只移动一次;
  • 替换空格之后,空格由一个字符变成了三个字符,因此字符串会变长。如果在原来字符串的基础上进行替换,就有可能覆盖修改在该字符串后面的内存。如果是创建新的字符串并在新的字符串上进行替换,则可以自己分配足够多的内存。

后记

如果你同我一样想要努力学好数据结构与算法、想要刷 LeetCode 和剑指 offer,欢迎关注我 GitHub 上的 LeetCode 题解:awesome-java-notes

你可能感兴趣的:(剑指offer)