面试算法题之旋转置换,旋转跳跃我闭着眼

轮转数组

给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。

借用临时数组

我们可以新建一个临时数组,用于存储旋转后的元素。首先获取数组的长度n,并计算k%nk值限制在数组nums长度范围内,避免不必要的旋转。创建一个临时数组ans,在第一个循环中,从位置n-k开始,将nums向量中的元素逐个添加到ans向量中。在第二个循环中,从位置 0 开始,将 nums 向量中的元素逐个添加到 ans 向量中。执行完两个循环后就得到了旋转后的数组,但题意需要通过参数nums传递结果,所以通过最后一个循环将数组ans中的元素逐个复制回数组nums中。

cpp 复制代码
class Solution {
public:
    void rotate(vector<int>& nums, int k) {
        int n = nums.size();
        k %= n;
        vector<int> ans;
        for(int i=n-k;i<n;i++) {
            ans.push_back(nums[i]);
        }
        for(int i=0;i<n-k;i++) {
            ans.push_back(nums[i]);
        }
        for(int i=0;i<n;i++) {
            nums[i] = ans[i];
        }
    }
};

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

多次翻转数组

实际上我们将数组旋转后,最终结果是将末尾 <math xmlns="http://www.w3.org/1998/Math/MathML"> k k </math>k位数移动至数组开头,部分数组元素排序并没有改变。那么如何可以快速将末尾元素调换至数组开头呢?

nums = [1,2,3,4,5,6,7,8], k = 2, n = 8,数组旋转后得到[7,8,1,2,3,4,5,6]

我们先将整个数组翻转,得到[8,7,6,5,4,3,2,1],这样末尾元素就移动到了数组开头,但元素顺序改变了。这时,我们将数组前 <math xmlns="http://www.w3.org/1998/Math/MathML"> k k </math>k位分为一组,其余元素为另一组。分别对这两组执行一次数组翻转,这样元素顺序也就调转回来了,得到结果[7,8,1,2,3,4,5,6]

cpp 复制代码
class Solution {
public:
    void reverse(vector<int>& nums, int s, int e) {
        while(s < e) {
            swap(nums[s++], nums[e--]);
        }
    }
    void rotate(vector<int>& nums, int k) {
        int n = nums.size();
        k %= n;
        reverse(nums, 0, n-1);
        reverse(nums, 0, k-1);
        reverse(nums, k, n-1);
    }
};

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

分组循环

在上述使用临时数组方案中,临时数组是为了避免替换位置的元素被覆盖。当然,我们也可以使用一个临时变量去记录。

我们假设将数组分为cnt组,每个组的大小为n/cnt。这里分组数cnt计算如下:

假设从起点开始到最终回到起点共经历m个元素,恰好走了t圈,那么有 <math xmlns="http://www.w3.org/1998/Math/MathML"> t n = m k tn=mk </math>tn=mk,由于是第一次返回到起点,则 <math xmlns="http://www.w3.org/1998/Math/MathML"> t t </math>t一定要小,即为 <math xmlns="http://www.w3.org/1998/Math/MathML"> m 、 k m、k </math>m、k的最小公倍数 <math xmlns="http://www.w3.org/1998/Math/MathML"> l c m ( n , k ) lcm(n,k) </math>lcm(n,k)。得到 <math xmlns="http://www.w3.org/1998/Math/MathML"> m = l c m ( n , k ) k m=\frac{lcm(n,k)}{k} </math>m=klcm(n,k),即一组遍历会经过 <math xmlns="http://www.w3.org/1998/Math/MathML"> m m </math>m个元素。那么有 <math xmlns="http://www.w3.org/1998/Math/MathML"> c n t = n m = n k l c m ( n , k ) = g c d ( n , k ) cnt=\frac{n}{m}=\frac{nk}{lcm(n,k)}=gcd(n,k) </math>cnt=mn=lcm(n,k)nk=gcd(n,k),其中 <math xmlns="http://www.w3.org/1998/Math/MathML"> l c m lcm </math>lcm表示最小公倍数, <math xmlns="http://www.w3.org/1998/Math/MathML"> g c d gcd </math>gcd表示最大公约数。

第一组从位置 0 开始,tmp = nums[0],根据题意,位置 0 的元素会被置于 <math xmlns="http://www.w3.org/1998/Math/MathML"> j = ( 0 + k ) m o d     n j=(0+k) \mod n </math>j=(0+k)modn的位置,交换tmpnums[j],此时tmp已经更新,即被替换的j位置的原元素。之后,再观察j位置,交换tmpnums[(j+k)%n],再次更新了tmp。如此依次处理数组内的元素,直至回到初始位置 0。

接下来每组亦是如此依次处理数组内的元素,直至回到初始位置 0。

nums = [1,2,3,4,5,6,7,8], k = 2, n = 8,如此计算kn的最大公约数为 2 ,我们可以将数组分成 2 组,[1,3,5,7][2,4,6,8],变换过程如下图。

cpp 复制代码
class Solution {
public:
    void rotate(vector<int>& nums, int k) {
        int n = nums.size();
        k %= n;
        int cnt = gcd(n,k);
        for(int i=0; i<cnt; i++) {
            int curr = i;
            int tmp = nums[i];
            do {
                int j = (curr + k) % n;
                swap(nums[j], tmp);
                curr = j;
            }while(i != curr) ;
        }
    }
};

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

旋转链表

给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。

合并成循环链表

旋转链表与旋转数组不同,不经历一次遍历无法确定链表的长度 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n。另一个不同点在于移动一个链表元素不需要整体元素移动。

利用这点特性,我们可以先将链表合并成环,并在链表的 <math xmlns="http://www.w3.org/1998/Math/MathML"> n − ( k m o d     n ) n-(k \mod n) </math>n−(kmodn)处断开,如此就可以得到旋转后的链表。具体如何操作呢?

我们先定义一个迭代指针p,用于遍历链表记录链表长度 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n,此时p指针正指向链表尾部元素,并将链表头尾连接。

知道链表长度 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n后,由此就可以得到需要再向前移动p指针的步数 <math xmlns="http://www.w3.org/1998/Math/MathML"> c n t = n − ( k m o d     n ) cnt = n-(k \mod n) </math>cnt=n−(kmodn),再移动p指针 <math xmlns="http://www.w3.org/1998/Math/MathML"> c n t cnt </math>cnt步,此时p指针正指向旋转后链表的尾部元素,定义ans记录新链表的头部元素,再断开链表就完成链表的旋转啦。

cpp 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* rotateRight(ListNode* head, int k) {
        if(k==0 || head==nullptr || head->next==nullptr)
            return head;
        int n = 1;
        ListNode* p = head;
        while(p->next != nullptr) {
            p = p->next;
            n++;
        }
        p->next = head;
        // 需要移动的步数
        int cnt = n - k % n;
        while(cnt-- > 0) {
            p = p->next;
        }
        ListNode* ans = p->next;
        p->next = nullptr;
        return ans;
    }
};

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

相关推荐
hsling松子4 小时前
使用PaddleHub智能生成,献上浓情国庆福
人工智能·算法·机器学习·语言模型·paddlepaddle
dengqingrui1234 小时前
【树形DP】AT_dp_p Independent Set 题解
c++·学习·算法·深度优先·图论·dp
C++忠实粉丝4 小时前
前缀和(8)_矩阵区域和
数据结构·c++·线性代数·算法·矩阵
ZZZ_O^O5 小时前
二分查找算法——寻找旋转排序数组中的最小值&点名
数据结构·c++·学习·算法·二叉树
CV-King5 小时前
opencv实战项目(三十):使用傅里叶变换进行图像边缘检测
人工智能·opencv·算法·计算机视觉
代码雕刻家6 小时前
数据结构-3.9.栈在递归中的应用
c语言·数据结构·算法
雨中rain6 小时前
算法 | 位运算(哈希思想)
算法
Kalika0-07 小时前
猴子吃桃-C语言
c语言·开发语言·数据结构·算法
sp_fyf_20248 小时前
计算机前沿技术-人工智能算法-大语言模型-最新研究进展-2024-10-02
人工智能·神经网络·算法·计算机视觉·语言模型·自然语言处理·数据挖掘
我是哈哈hh9 小时前
专题十_穷举vs暴搜vs深搜vs回溯vs剪枝_二叉树的深度优先搜索_算法专题详细总结
服务器·数据结构·c++·算法·机器学习·深度优先·剪枝