数组的旋转与移位:左旋、右旋与循环移位算法

数组的旋转与移位,说白了就是让数组里的元素「转起来」。

我刚开始学C语言时,觉得这玩意儿不就是把元素挪个位置吗?后来在项目中做数据缓冲区管理,才发现这玩意儿坑不少。你想想看,一个环形队列、一个滚动显示、甚至某些加密算法,底层都离不开数组移位。

今天咱们就把左旋、右旋、循环移位这几个东西彻底讲透。

什么是数组旋转?

先给个直观感受。

假设你有一个数组:[1, 2, 3, 4, 5]

  • 左旋1位:把第一个元素移到末尾 → [2, 3, 4, 5, 1]
  • 右旋1位:把最后一个元素移到开头 → [5, 1, 2, 3, 4]

嗯,就这么简单。但实际写代码时,要考虑效率、边界条件、以及——别把数据搞丢了。

核心概念:旋转的本质是「分段交换」。左旋k位,相当于把前k个元素整体搬到后面;右旋k位,相当于把后k个元素整体搬到前面。

左旋算法

左旋k位,就是把数组前k个元素「切下来」放到末尾。

我习惯用三种方式实现,每种都有它的适用场景。

方法一:暴力移位法

最直接的想法——一次移一位,重复k次。

void leftRotateByOne(int arr[], int n) {
    int temp = arr[0];
    for (int i = 0; i < n - 1; i++) {
        arr[i] = arr[i + 1];
    }
    arr[n - 1] = temp;
}

void leftRotate(int arr[], int n, int k) {
    k = k % n;  // 防止k大于n
    for (int i = 0; i < k; i++) {
        leftRotateByOne(arr, n);
    }
}

这个方法好理解,但效率低。时间复杂度O(n*k),n大了就扛不住。我在做嵌入式项目时用过一次,数组只有几十个元素,倒也没啥问题。但如果你处理的是上万个元素,千万别这么干。

方法二:临时数组法

开一个临时数组,把前k个元素存起来,剩下的往前挪,再把临时数组的内容放回末尾。

void leftRotate(int arr[], int n, int k) {
    k = k % n;
    int temp[k];
    
    // 1. 保存前k个元素
    for (int i = 0; i < k; i++) {
        temp[i] = arr[i];
    }
    
    // 2. 剩余元素左移
    for (int i = k; i < n; i++) {
        arr[i - k] = arr[i];
    }
    
    // 3. 把temp放回末尾
    for (int i = 0; i < k; i++) {
        arr[n - k + i] = temp[i];
    }
}

时间复杂度O(n),空间复杂度O(k)。

这里有个坑——C语言里变长数组(VLA)在有些编译器上不支持。我建议你直接用动态内存分配,或者把k设成固定大小。

方法三:反转法(最优雅)

这个方法我第一次看到时,觉得太巧妙了。它基于一个数学观察:

左旋k位 = 反转前k个 + 反转后n-k个 + 反转整个数组

void reverse(int arr[], int start, int end) {
    while (start < end) {
        int temp = arr[start];
        arr[start] = arr[end];
        arr[end] = temp;
        start++;
        end--;
    }
}

void leftRotate(int arr[], int n, int k) {
    k = k % n;
    reverse(arr, 0, k - 1);      // 反转前k个
    reverse(arr, k, n - 1);      // 反转后n-k个
    reverse(arr, 0, n - 1);      // 反转整个数组
}

时间复杂度O(n),空间复杂度O(1)。

我曾经在面试中被问到这道题,当时我写了暴力法,面试官笑了笑说「还有更好的吗?」——嗯,从那以后我就把反转法刻在脑子里了。

右旋算法

右旋和左旋本质上是一回事。右旋k位,相当于左旋n-k位。

所以你可以直接复用左旋的代码:

void rightRotate(int arr[], int n, int k) {
    k = k % n;
    leftRotate(arr, n, n - k);  // 右旋k = 左旋n-k
}

当然,你也可以用反转法直接实现右旋:

void rightRotate(int arr[], int n, int k) {
    k = k % n;
    reverse(arr, 0, n - 1);      // 反转整个数组
    reverse(arr, 0, k - 1);      // 反转前k个
    reverse(arr, k, n - 1);      // 反转后n-k个
}

你看,反转的顺序变了。左旋是先局部再整体,右旋是先整体再局部。这个细节容易搞混,我建议你画个图记一下。

循环移位算法

循环移位和旋转其实是一回事,只是叫法不同。但有一种特殊情况值得单独拿出来讲——原地循环移位,也就是不开额外数组,只用O(1)额外空间。

反转法就是典型的原地循环移位。除此之外,还有一种「环状替换法」,它像多米诺骨牌一样,一个元素把另一个元素挤走,被挤走的再去挤下一个,直到回到起点。

void leftRotateCycle(int arr[], int n, int k) {
    k = k % n;
    int moved = 0;  // 已经移动的元素个数
    
    for (int start = 0; moved < n; start++) {
        int current = start;
        int prev = arr[start];
        
        do {
            int next = (current + k) % n;
            int temp = arr[next];
            arr[next] = prev;
            prev = temp;
            current = next;
            moved++;
        } while (start != current);
    }
}

这个方法的时间复杂度也是O(n),空间O(1)。但代码逻辑稍微绕一点,我一般只在面试时秀一下,实际项目中还是用反转法,因为可读性更好。

注意:环状替换法有一个隐藏陷阱——当n和k不互质时,一次循环无法覆盖所有元素。比如n=6, k=2,你需要多个起点才能完成全部移动。上面的代码用了一个外层循环来处理这个问题。

三种方法对比

方法 时间复杂度 空间复杂度 适用场景
暴力移位法 O(n*k) O(1) n很小,k也很小
临时数组法 O(n) O(k) k不大,且允许额外空间
反转法 O(n) O(1) 通用,推荐首选
环状替换法 O(n) O(1) 追求极致空间效率

避坑指南

我这些年写数组移位踩过的坑,给你列几个:

  • k可能大于n:一定要取模,否则数组越界。我曾经在代码里忘了这茬,结果调试了一下午。
  • k为负数:有些场景下k可能是负数,表示反向旋转。处理方式:k = (k % n + n) % n
  • n为0或1:边界情况,旋转没有意义,直接返回。
  • 数组类型:上面的例子都是int数组,但原理适用于任何类型。如果你写通用函数,可以用void* + memcpy。

我的习惯:在项目里,我一般把左旋和右旋封装成两个函数,底层统一用反转法实现。这样调用方不用关心具体算法,代码也干净。

知识体系总览

下面这张图把数组旋转与移位的核心逻辑串起来了,你可以对照着看。

数组旋转与移位 · 知识体系 数组旋转/移位 左旋 k 位 右旋 k 位 暴力移位法 临时数组法 反转法 ★ 左旋 n-k 位 反转法 ★ 环状替换法 关键要点 ① k 必须取模 (k % n) ② 反转法最推荐 (O(n)时间, O(1)空间) ③ 左旋与右旋可互相转换 ④ 注意 n=0 或 n=1 的边界

总结

数组旋转与移位,说白了就是「分段交换」四个字。

我个人最推荐反转法,代码简洁、效率高、不容易出错。如果你在面试中遇到这道题,直接写反转法,面试官一般都会点头。

嗯,今天就到这里。代码多敲几遍,自然就熟了。


公众号:蓝海资料掘金营,微信deep3321