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

轮转数组

给定一个整数数组 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)。

相关推荐
Tipriest_8 分钟前
C++ 的 ranges 和 Python 的 bisect 在二分查找中的应用与实现
c++·python·算法·二分法
晨晖21 小时前
顺序查找:c语言
c语言·开发语言·算法
LYFlied1 小时前
【每日算法】LeetCode 64. 最小路径和(多维动态规划)
数据结构·算法·leetcode·动态规划
Salt_07282 小时前
DAY44 简单 CNN
python·深度学习·神经网络·算法·机器学习·计算机视觉·cnn
货拉拉技术2 小时前
AI拍货选车,开启拉货新体验
算法
MobotStone2 小时前
一夜蒸发1000亿美元后,Google用什么夺回AI王座
算法
Wang201220132 小时前
RNN和LSTM对比
人工智能·算法·架构
xueyongfu2 小时前
从Diffusion到VLA pi0(π0)
人工智能·算法·stable diffusion
永远睡不够的入3 小时前
快排(非递归)和归并的实现
数据结构·算法·深度优先
cheems95273 小时前
二叉树深搜算法练习(一)
数据结构·算法