跳转至

AC102-数组循环左移

将 n 个整数存放在数组 R 中,设计尽可能高效的算法,将 R 序列循环左移 p 个位置(0 < p < n),即将 R 中的数据由 (X₀, X₁, ..., Xₙ₋₁) 变换为 (Xₚ, Xₚ₊₁, ..., Xₙ₋₁, X₀, X₁, ..., Xₚ₋₁)。(2010 年统考题)

切片为AB33

代码实现

C
/**
 * 反转数组的指定区间
 * @param R[] 待处理数组
 * @param start 区间起始索引(包含)
 * @param end   区间结束索引(包含)
 */
void Reverse(int R[], int start, int end) {
    // 双指针法反转数组区间
    while (start < end) {
        // 交换两端元素
        int temp = R[start];
        R[start] = R[end];
        R[end] = temp;

        // 移动指针
        start++;
        end--;
    }
}

/**
 * 数组循环左移 p 个位置
 * @param R[] 待处理数组
 * @param n   数组长度
 * @param p   左移的位置数(0 < p < n)
 */
void change(int R[], int n, int p) {
    // 第一步:整体反转整个数组
    Reverse(R, 0, n - 1);

    // 第二步:反转前 n-p 个元素
    Reverse(R, 0, n - p - 1);

    // 第三步:反转后 p 个元素
    Reverse(R, n - p, n - 1);
}

算法执行过程图解

假设 n=8,p=3,初始数组为 [X0, X1, X2, X3, X4, X5, X6, X7]

初始状态:

Text Only
[X0, X1, X2, X3, X4, X5, X6, X7]

第一步:整体反转

调用 Reverse(R, 0, 7) 后:

Text Only
[X7, X6, X5, X4, X3, X2, X1, X0]

第二步:反转前 n-p 个元素

调用 Reverse(R, 0, 4) 后:

Text Only
[X3, X4, X5, X6, X7, X2, X1, X0]

第三步:反转后 p 个元素

调用 Reverse(R, 5, 7) 后:

Text Only
[X3, X4, X5, X6, X7, X0, X1, X2]

最终结果即为循环左移 p 个位置后的数组。

复杂度分析

时间复杂度:O(n)

详细分析:

  1. 第一次反转
  2. 反转整个数组,操作次数为 n/2
  3. 时间复杂度 O(n)

  4. 第二次反转

  5. 反转前 n-p 个元素,操作次数为 (n-p)/2
  6. 时间复杂度 O(n-p)

  7. 第三次反转

  8. 反转后 p 个元素,操作次数为 p/2
  9. 时间复杂度 O(p)

  10. 总时间复杂度

    Text Only
    T(n) = O(n/2) + O((n-p)/2) + O(p/2)
         = O(n)
    

空间复杂度:O(1)

  • 只使用了常数个额外变量(start, end, temp)
  • 原地操作,不需要额外的存储空间
  • 空间复杂度为 O(1)

算法原理深入解析

1. 反转操作的数学基础

重要性质:

  • 对一个序列进行两次完全反转可以恢复原序列
  • 反转操作具有可逆性

数学推导:

设原始序列为 S = [X0, X1, ..., Xn-1]

  1. 整体反转

    Text Only
    Reverse(S, 0, n-1) → [Xn-1, Xn-2, ..., X0]
    

  2. 分段反转

  3. 前 n-p 个元素反转:
    Text Only
    Reverse([Xn-1, ..., Xp], 0, n-p-1) → [Xp, Xp+1, ..., Xn-1]
    
  4. 后 p 个元素反转:

    Text Only
    Reverse([Xp-1, ..., X0], n-p, n-1) → [X0, X1, ..., Xp-1]
    

  5. 组合结果

    Text Only
    Final Result = [Xp, Xp+1, ..., Xn-1, X0, X1, ..., Xp-1]
    

2. 算法正确性证明

性质1:每个元素的最终位置正确

  • 元素 Xi (i >= p) 最终位置应为 i-p
  • 元素 Xi (i < p) 最终位置应为 n-p+i

通过三次反转可以验证每个元素都到达了正确位置。

性质2:相对顺序保持不变

  • 每次反转都是整体操作,不会改变子序列内部的相对顺序
  • 因此最终结果保持了原始序列的相对顺序

3. 边界情况分析

  1. p = 1
  2. 整体反转后再分两部分反转
  3. 结果正确

  4. p = n-1

  5. 实际等价于右移 1 位
  6. 算法仍能正确处理

  7. p = n/2

  8. 特殊情况:正好对半分
  9. 算法依然适用

关键知识点延伸

1. 原地算法的优势

  • 空间效率高:只需 O(1) 额外空间
  • 缓存友好:数据局部性好,访问连续内存
  • 实现简单:只需要基本的交换操作

2. 扩展应用:循环右移

C
/**
 * 数组循环右移 p 个位置
 * @param R[] 待处理数组
 * @param n   数组长度
 * @param p   右移的位置数(0 < p < n)
 */
void RotateRight(int R[], int n, int p) {
    // 右移 p 位等价于左移 n-p 位
    change(R, n, n - p);
}

3. 通用旋转函数

C
/**
 * 数组旋转函数
 * 支持正负方向旋转
 * @param R[] 待处理数组
 * @param n   数组长度
 * @param k   旋转步数(正数表示右移,负数表示左移)
 */
void Rotate(int R[], int n, int k) {
    if (k == 0 || n <= 1) return;  // 无需旋转

    // 处理负数和超过长度的情况
    k %= n;
    if (k < 0) k += n;  // 转换为正旋转

    // 使用三次反转实现旋转
    Reverse(R, 0, n - 1);      // 整体反转
    Reverse(R, 0, k - 1);      // 反转前 k 个
    Reverse(R, k, n - 1);      // 反转后 n-k 个
}

4. 分块移动法实现

C
/**
 * 分块移动法实现循环左移
 * 适用于无法使用反转的情况
 */
void BlockSwapLeftShift(int R[], int n, int p) {
    int i = p, j = n - p;

    while (i != j) {
        if (i < j) {
            // 将后部分的前 i 个元素与前部分交换
            for (int k = 0; k < i; k++) {
                swap(&R[p-i+k], &R[p+j-i+k]);
            }
            j -= i;
        } else {
            // 将前部分的后 j 个元素与后部分交换
            for (int k = 0; k < j; k++) {
                swap(&R[p-j+k], &R[p-i+j+k]);
            }
            i -= j;
        }
    }

    // 最后一次交换
    for (int k = 0; k < i; k++) {
        swap(&R[p-i+k], &R[p+j-i+k]);
    }
}

5. 应用场景

  1. 字符串处理:循环移位加密、文本处理
  2. 图像处理:像素矩阵旋转
  3. 数据缓冲区管理:循环队列实现
  4. 多媒体处理:音频视频帧的循环移动

算法特点总结

  1. 高效性:时间复杂度 O(n),空间复杂度 O(1)
  2. 通用性:可以通过参数调整实现左右任意方向的旋转
  3. 稳定性:保持元素的相对顺序不变
  4. 简洁性:实现简单,逻辑清晰
  5. 可靠性:适用于各种边界情况

记忆要点

  • 三次反转法:整体反转 + 分段反转
  • 反转顺序:先整体,再前段,最后后段
  • 时间复杂度:线性时间 O(n)
  • 空间复杂度:原地操作 O(1)
  • 扩展性:可通过调整参数实现左右旋转