字符串左旋

友情提示:本文中的代码只为展示思考逻辑,本身并不严谨,而且没有处理极值等特殊情况,默认参数都在正常范围内,请勿在正式场合直接拷贝使用!!!

问题:

把字符串前面若干个字符移动到字符串的尾部,例如:字符串abcdef左旋2位,得到cdefab。
请实现左旋函数,要求对长度为n的字符串操作的时间复杂度为O(n),空间复杂度为O(1)。

思路一 : 直接位移

​ 此题要求空间复杂度为O(1), 所以我们按照最简单自然的想法,每次移动一位,左旋m位只要循环m次就可以了。先写个左旋一位的函数:

private char[] rotateLeftOne(char[] s) {
        char temp = s[0];
        int length = s.length;
        for (int i = 1; i < length; i++) {
            s[i - 1] = s[i];
        }
        s[length - 1] = temp;
        return s;
    }

然后,左移m位的话就是:

private char[] rotateLeft(char[] s, int m) {
        while (m > 0) {
            s = rotateLeftOne(s);
            m--;
        }
        return s;
    }

很容易可以计算出 时间复杂度为O(mn)* 空间复杂度为O(1)

思路二 : 三步翻转法

​ 我们换一种思路来看这个问题,跳出向左移动这个想法。
​ 将这个字符串拆成两部分来看,左旋出去的那段设为X,剩余部分设为Y。要得到YX就可以先将X反转,再将Y反转,最后将整个字符串反转。举个例子:

​ 假如现在要将abcdef这个字符串左旋3位,按照上面的想法需要三步:
​ 1.需要左旋的3位反转得到:cba def
​ 2.剩余部分反转得到:cba fed
​ 3.再整体反转得到:defabc

​ 盗张图给大家看的更清晰一些:

java_algorithm_1_1.png

根据三步翻转法的思路,我们写一下代码,翻转某一段的函数可以这么写:

private static char[] reverseCharArray(char[] s, int start, int end) {
    int length = (end - start) == 0 ? 0 : end - start + 1; // 起始结束同一位置不处理
    int n = length >> 1;
    for (int i = 0; i < n; i++) {
        char temp = s[start + i];
        s[start + i] = s[end - i];
        s[end - i] = temp;
    }
    return s;
}

然后,左旋的函数可以写成:

private static char[] rotateString(char[] s, int m) {
        if(m==0)
          return s;
        s = reverseCharArray(s, 0, m - 1);
        s = reverseCharArray(s, m, s.length - 1);
        s = reverseCharArray(s, 0, s.length - 1);
        return s;
}

最后我们看一下效率问题:第一步循环了 m/2 次,第二步循环了(n-m)/2次,第三次循环了n/2次
时间复杂度为O(n) 空间复杂度为O(1)
很高效的方法了

当然我们的探索不能只到这里,继续

思路三 : 指针翻转法

​ 思考一下,对字符串的左旋,我们能不能看作是前几位和后面几位交换位置呢?举个特殊的例子来理解一下:

​ 假如有abcdefg这个串需要左旋1位,那么我们可以把需要移动的字符a向后移动,与他之后的每个元素交换位置,直到它到达末尾。这样我们就得到了需要的结果(bcdefa)。

​ 当然,一个字符很容易处理,我们只需要把它和后面的元素依次交换位置就可以,如果是多个字符需要移动呢?比如上面的例子,需要左旋2位,该怎么处理呢 ?

​ 按照我们之前的方法来走一下:
​ 1.字符ab后移,和后面的元素换位:cdabefg
​ 2.继续后移:cdefabg
​ 问题来了,后面位置不够了怎么破?
​ 3.先把有位置的移走,这样左旋剩余的字符已经全部移动到正确位置,我们只需要对左旋的串做一下位置调整:cdefgba
​ 4.位置调整,可以看作是ba这个字符串,左旋1位,重复上面的操作就可以:cdefgab

还不理解?再盗图一张:

java_algorithm_1_2.png

明白一些了吧,有了思路我们开始撸代码

private static char[] forwardIteratorReverse(char[] s, int m) {
    return forwardIteratorReverse(s, 0, m);
}

private static char[] forwardIteratorReverse(char[] s, int start, int middle) {
    if (middle <= start)
        return s;
    int mod = (s.length - middle) % (middle - start); // 最后会剩余几位
    while (middle < s.length) {
        char temp = s[start];
        s[start] = s[middle];
        s[middle] = temp;
        middle++;
        start++;
    }
    // 不需要再调整位置了
    if (mod == 0)
        return s;
    else {
        return forwardIteratorReverse(s, start, s.length - mod);
    }
}

当然也可以严格按照盗图上的思路,每次移动m个位置标记为一次,在每次开始之前进行特殊情况处理:

写法二:

private static char[] forwardIteratorReverse2(char[] s, int m) {
        return m == 0||m==s.length ? s : forwardIteratorReverse2(s, 0, m);
    }

private static char[] forwardIteratorReverse2(char[] s, int start, int middle) {
    int forepartLength = middle - start;
    int secondHalfLength = s.length - middle;
    // 前段小于后段,可以安全移动
    if (forepartLength < secondHalfLength) {
        for (int i = 0; i < forepartLength; i++) {
            char temp = s[start];
            s[start] = s[middle];
            s[middle] = temp;
            middle++;
            start++;
        }
    }
    // 前段等于后段,交换完成即结束
    else if (forepartLength == secondHalfLength) {
        for (int i = 0; i < forepartLength; i++) {
            char temp = s[start];
            s[start] = s[middle];
            s[middle] = temp;
            middle++;
            start++;
        }
        return s;
    }
    // 前段大于后段,middle位置不变,进行交换
    else {
        int middleCopy = middle;
        for (int i = 0; i < secondHalfLength; i++) {
            char temp = s[start];
            s[start] = s[middleCopy];
            s[middleCopy] = temp;
            middleCopy++;
            start++;
        }
    }
    return forwardIteratorReverse2(s, start, middle);
}

根据代码逻辑,可计算出 时间复杂度为O(n) 空间复杂度为O(1) ,满足题目的要求。

思路四 : 循环位移法

​ 下面我们从整体角度来考虑一下。一个字符串,实质发生左旋m位后,每个字符都变动了位置。需要左旋的几位会移动到字符串末尾,而剩余的位置会向左移动m位。既然每个位置都做了变动,那么能不能找出一个位置会被哪个位置的元素替代呢?举个例子:

​ abcd这个字符串左旋1位:

​ 1.我们先把需要左移的位置a取出来->_bcd

​ 2.a空出来的位置会被b占了->b_cd

​ 3.而b空出来的位置会被c占了,c空的位置会被d占了->bc_d ->bcd_

​ 4.最后d空出来的位置是被a占了,把之前移出去的a放到d原来的位置 ->bcda

​ 这样就得到了我们想要的结果。

再来举个稍微复杂一点的例子帮助理解:

​ abcde左旋2位:

​ 1.还是老步骤,将a移出去 ->_bcde

​ 2.a的位置被2个位置后的c占了 ->cb_de

​ 3.c原来的位置被2位后的e占了 ->cbed_

​ 4.e原来的位置被2位后的b占了(因为e是最后一位,所以被左旋的b占了) ->c_edb

​ 5.b原来的位置被2位后的d占了 ->cde_b

​ 6.d原来的位置被2位后的a占了,把a移回来 ->cdeab

​ 也得到了结果。很容易发现规律,每次移动都把一个元素放到了正确的位置上,每个元素移动的距离都是需要左旋的m位。根据上面的规律,我们不难写出计算的公式{m%n,2m%n...}。

​ 到这里当然没有结束,刚才上面的问题是一种特殊情况(m和n互为质数)。如果不是互为质数会发生什么问题呢? 还是第一个例子,我们改成左旋2位。按照上面的思路,a的位置被c占了,c的位置被a占了。这样就形成了一个闭环,不管b和c的事了。这样的话,我们想要讲所有的元素都放到正确位置就需要2次循环。这里需要仔细理解清楚。

​ 这里其实使用了一个数学定理:若有两个正整数m、n,且gcd(m,n)=d,那么序列{m%n,2m%n, 3m%n,..., nm%n}一定是{0, d, 2d,..., n-d}的某个排列并重复出现d次。比如若m=6,n=8,d=gcd(m,n)=2,那么{6%8, 12%8,18%8,..., 48%8}即为{0,2,4,6}的某个排列并重复两次,事实上也正是{6,4,2,0, 6,4,2, 0}。特别地,若m、n互素,d=1,那么序列{m%n,2m%n,3m%n,...,(n-1)m%n}实际上就是{1, 2,3,..., n-1}的某个排列。

gcd(m,n) 是求m和n的最大公约数的一种方式:辗转相除法,这里我们不做讨论,后面会有一篇专门探讨求最大公约数的实现。

​ 有了上面的思考和理论,终于可以下手写代码了。

private static char[] randomInterator(char[] s, int m) {
        int length = s.length;
        int d = gcd(m, length);
        for (int i = 0; i < d; i++) {
            int n = 2;
            int start = i + m % length;
            int lastPosition = start;
            int position = i + (n * m) % length;
            char temp = s[start];
            while (position != start) {
                s[lastPosition] = s[position];
                lastPosition = position;
                n++;
                position = i + (n * m) % length;
            }
            s[lastPosition] = temp;
        }
        return s;
    }

有任何问题欢迎联系我改正。

参考:
程序员编程艺术:第一章、左旋转字符串
【STL源码剖析读书笔记】【第6章】算法之rotate算法

你可能感兴趣的:(字符串左旋)