方法一:使用额外数组
思路:
(i + k)%numsSize
(常用于解决循环队列问题)代码实现(提交已通过):
void rotate(int* nums, int numsSize, int k)
{
int tmp[numsSize];//创建额外数组
//将原数组拷贝到额外数组
for (int i = 0; i < numsSize; i++)
{
tmp[i] = nums[i];
}
//按照额外数组的值按 右移关系 放到原数组
for (int i = 0; i < numsSize; i++)
{
nums[(i + k) % numsSize] = tmp[i];
}
}
复杂度分析:
时间复杂度: O(n),其中 n 为数组的长度。
空间复杂度: O(n)。
方法二:环状替换
思路: 方法一中使用了额外数组的原因是若我们直接把每个数字放到它最后的位置,这样被放置的元素会被覆盖掉。其实,我们可以创建一个临时变量temp,可以将被替换的元素“备份”起来。避免了额外数组的开销,空间复杂度为O(1).
画图:
分析: 对于一个长度为numsSize 的数组,整体右移k个位置
当numsSize = 4,k = 2(最大公约数为2)
方式一:代码实现(提交已通过):
void rotate(int* nums, int numsSize, int k)
{
k = k % numsSize;
int count = 0;//记录已交换个数
for (int start = 0; count < numsSize; start++)
{
int cur = start;//从0开始
int pre = nums[cur];//pre-需要右移的元素
do {
int next = (cur + k) % numsSize;//被替换元素的下标
int temp = nums[next];//对被替换元素“备份”
//交换
nums[next] = pre;
pre = temp;
cur = next;
count++;//交换完成,count+1
} while (start != cur);
}
方式二:代码实现(提交已通过):
//递归实现返回最大公约数
int gcd(int a, int b)
{
return b ? gcd(b, a % b) : a;
}
void rotate(int* nums, int numsSize, int k)
{
k = k % numsSize;
int i = 0;
for (int start = 0; i < gcd(numsSize,k); start++,i++)
{
int cur = start;//从0开始
int pre = nums[cur];//需要右移的元素
do {
int next = (cur + k) % numsSize;//被替换元素下标
int temp = nums[next];//对被替换元素“备份”
//交换
nums[next] = pre;
pre = temp;
cur = next;
} while (start != cur);
}
}
复杂度分析
时间复杂度: O(n),其中 n 为数组的长度。每个元素只会被遍历一次。
空间复杂度: O(1)。我们只需常数空间存放若干变量。
方法三:三步逆置法(最优)
思路: 对于长度为numsSize,整体右移 k 的数组
以上操作均是对该数组操作,即原地算法
画图:
题目要求空间为O(1),即要在原数组上操作,妙处是逆置。该方法理解起来十分容易,但却难以想到,真是奇技淫巧。
代码实现(提交已通过):
void Reverse(int* nums, int left, int right)
{
while (left < right)
{
int tmp = nums[right];
nums[right] = nums[left];
nums[left] = tmp;
++left;
--right;
}
}
void rotate(int* nums, int numsSize, int k)
{
if (k >= numsSize)
k %= numsSize;
//后k个数逆置
Reverse(nums, numsSize - k, numsSize - 1);
//前n-k个数逆置
Reverse(nums, 0, numsSize - k - 1);
//整体逆置
Reverse(nums, 0, numsSize - 1);
}
关于Reverse逆置函数实现方法有多种,也可以是
void swap(int* a, int* b)
{
int t = *a;
*a = *b;
*b = t;
}
void Reverse(int* nums, int start, int end)
{
while (start < end)
{
swap(&nums[start], &nums[end]);
start++;
end--;
}
}
复杂度分析
时间复杂度: O(n),其中 n 为数组的长度。每个元素被翻转两次,一共 n 个元素,因此总时间复杂度为 O(2n)=O(n)。
空间复杂度: O(1)。
仙人指路➡️: [力扣27 - 移除元素]
(注:题目要求是 原地 修改输入数组,仅对原数组操作。)
方法一:通用解法
思路: 遍历数组,把与val不相等的元素往前丢
画图:偷懒一下 比较简单
代码实现(提交已通过):
int removeElement(int* nums, int numsSize, int val)
{
//k记录不等于val的个数
int k = 0;
for (int i = 0; i < numsSize; i++)
{
if (nums[i] != val)
nums[k++] = nums[i];
}
return k;//新的顺序表的长度
}
方法二:双指针
思路: 和方法一核心思想一样。
输出数组的长度一定是小于或等于输入数字的长度,因此直接把输出数组写作输入数组上。
右指针right 指向当前将要处理的元素,左指针 left 指向下一个将要赋值的位置。
数组区间 [0,left) 中的元素都不等于val。当左右指针遍历完输入数组以后,left 的值就是输出数组的长度。
代码实现(提交已通过):
int removeElement(int* nums, int numsSize, int val)
{
int left = 0;
for (int right = 0; right < numsSize; right++)
{
if (nums[right] != val)
{
nums[left] = nums[right];
left++;
}
}
return left;
}
这样的算法在最坏情况下(输入数组中没有元素等于val),左右指针各遍历了数组一次。
⚡双指针优化:
若需要移除的元素恰好在数组的开头,如[1,2,3,4,5],val = 1
按照未优化前的代码,会把每一个元素都左移1位。而题目中说到[元素的顺序可以改变]
因此,⚡优化思路: 把最后一个元素5移到数组开头,取代元素1,变成[5,2,3,4]
。
算法:
右指针 right 指向数组尾部,即从尾部开始处理元素,左指针 left 依旧指向数组开头,表示即将要赋值的位置。
左指针 left 指向的元素等于 val,右指针 right 指向的元素复制到左指针 left 的位置,然后right 左移1位(若复制过去的值还等于 val ,重复该操作,直到左指针指向的元素不等于val)
当左右指针重合时,说明已经遍历了数组所有元素。
⚡代码实现(提交已通过):
int removeElement(int* nums, int numsSize, int val)
{
int left = 0;
int right = numsSize;
while(left < right)
{
if(nums[left] == val)
{
nums[left] = nums[right - 1];
right--;
}
else
{
left++;
}
}
return left;
}
优化后的方法: 左右指针在最坏的情况下合起来只遍历了数组一次。且避免了需要保留的元素的重复赋值操作。
(注: 双指针可以用数组的形式,也可以用指针的形式,都是找到某个元素的一种方式。)
复杂度分析:
时间复杂度: O(n),其中 n 为序列的长度。我们只需要遍历该序列至多一次。
空间复杂度: O(1)。我们只需要常数的空间保存若干变量。
仙人指路➡️: [力扣88 - 合并两个有序数组]
要求: 时间复杂度要求为O(m+n),则考查 原地修改,将空间复杂度降低到O(1)。不需要额外的空间,将nums2放进nums1
方法一:逆向指针法
思路: nums1初始长度为 m + n,实际有m个元素,n个未放置元素的空间;原地修改时,为了避免从前往后遍历导致原数组元素被破坏,我们应从后往前遍历!
创建三个指针:
i、j:分别指向nums1和nums2的初始化元素数量的末位,即分别是m - 1 和 n - 1
dst:指向数组的末位,即m + n - 1
画图:
代码实现(提交已通过):
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n)
{
int i = m - 1;
int j = n - 1;
int dst = m + n - 1;
while (j >= 0)//必须保证nums2中的元素合并完
{
if (i >= 0 && nums1[i] > nums2[j])
{
nums1[dst] = nums1[i];
dst--;
i--;
}
else
{
nums1[dst] = nums2[j];
dst--;
j--;
}
//if()...else可用三目运算符优化为
//nums1[end--] = (i >= 0 && nums1[i] > nums2[j]) ? nums1[i--] : nums2[j--];
}
}
复杂度分析:
时间复杂度: O(m+n)。指针移动单调递减,最多移动 m+n 次,因此时间复杂度为 O(m+n)。
空间复杂度: O(1)。对nums1原地修改,不需要额外空间。
方法二:先合并再排序
思路: 比较容易想到:把nums2数组中的所有元素放到nums1数组中,然后用qsort函数对nums1数组整体进行排序。
画图: 略 ,上面这幅够形象了哈哈哈哈。
代码实现(提交已通过):
int cmp(int* a, int* b)
{
return *a - *b;
}
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n)
{
for(int i = 0; i < nums2Size; i++)
{
nums1[m + i] = nums2[i];
}
qsort(nums1,nums1Size,sizeof(int),cmp);
}
(注:qsort函数,我们在学习C指针进阶时已经模拟实现过。)
仙人指路➡️: 【详解C语言指针】我真的让C指针给我唱征服了~乌拉
复杂度分析:
时间复杂度: O((m+n)log(m+n))
排序序列长度为 m+n,套用快速排序的时间复杂度即可,平均情况为 O((m+n)log(m+n))。
空间复杂度: O(log(m+n))。
排序序列长度为 m+n,套用快速排序的空间复杂度即可,平均情况为 O(log(m+n))。
仙人指路➡️: [力扣26 - 删除有序数组中的重复项]
要求:
方法一:双指针
思路: 类似题2,我们有经验:看到关键字“原地修改”,需要保存可覆盖位置(slow)和观测位置(fast),立马想到双指针;
❕ 需要考虑:
算法:
nums[fast] != nums[fast - 1]
,说明nums[fast]和之前的元素都不同,则执行 nums[slow] = nums[fast];
然后slow+=1,指向下一个位置。nums[slow] = nums[fast];
,fast都得fast+=1,以达到遍历整个数组的目的。遍历完事后,从 nums[0] 到 nums[slow−1] 的每个元素都不相同且包含原数组中的每个不同的元素,因此新的长度即为 slow,返回 slow 即可。
int removeDuplicates(int* nums, int numsSize)
{
if (numsSize == 0)
return 0;
int slow = 1;
int fast = 1;
while (fast < numsSize)
{
if (nums[fast] != nums[fast - 1])
{
nums[slow] = nums[fast];
slow += 1;
}
fast += 1;
}
return slow;
}
复杂度分析
时间复杂度: O(n),其中 n 是数组的长度。快指针和慢指针最多各移动 n 次。
空间复杂度: O(1)。只需要使用常数的额外空间。
经验: 覆盖原有位置来有效利用空间,从而降低空间复杂度 - 双指针解法
移除链表元素有三种可能:
方法一:无哨兵位节点,直接删除法
思路:
当删除头结点时:
定义结构体指针tmp指向head,执行head = head->next;
后free掉tmp
当移除非头结点时:
定义结构体指针cur,指向head;cur遍历整个链表,当cur->next->val == val
时,定义结构体指针tmp指向cur->next,当cur指向它的下一个的下一个节点时,free掉tmp;当cur->next->val != val
时,cur = cur->next
画图:
(注意: C写数据结构题目时,没有手动在内存删除节点依旧可以通过,但内存使用空间会大一些。建议养成手动清理内存的习惯)
代码实现(提交已通过):
struct ListNode* removeElements(struct ListNode* head, int val)
{
//删除头结点
while (head != NULL && head->val == val)
{
struct ListNode* tmp = head;
head = head->next;
free(tmp);
//tmp=NULL;
}
//删除非头节点
struct ListNode* cur = head;
while (cur != NULL && cur->next != NULL)
{
if (cur->next->val == val)
{
struct ListNode* tmp = cur->next;
cur->next = cur->next->next;
free(tmp);
//tmp =NULL;
}
else
{
cur = cur->next;
}
}
return head;//当链表为空,以上操作均不执行,返回head即可
}
方法二:创建哨兵位头结点,统一所有情况
思路: 方法一需要分为移除头结点和非头结点,我们能否使本为头结点的变为非头结点从而统一为删除的只是非头结点呢?
我们可以找一个工具人做头结点,它就是哨兵位头节点guard(它不存储数据,只负责站岗,这就是合格的工具人)
画图:
有了工具人 - 哨兵位头结点guard,当要移除旧头结点时,就变得和移除非头结点一样了。
因此,哨兵结点的使用有利于链表代码的标准化,可以减少一些额外的分支判断。
代码实现(提交已通过):
struct ListNode* removeElements(struct ListNode* head, int val)
{
//创建哨兵位,统一所有情况
struct ListNode* guard = (struct ListNode*)malloc(sizeof(struct ListNode));
guard->next = head;
struct ListNode* cur = guard;
while(cur->next != NULL)
{
if(cur->next->val == val)
{
struct ListNode* tmp = cur->next;
cur->next = tmp->next;
free(tmp);
//tmp = NULL;
}
else
cur = cur->next;
}
head = guard->next;
free(guard);
//guard = NULL;
return head;
}
❕ 注意: 在最后return 头结点时,切记要head = guard->next;
,因为在此之前,head有可能会被free掉(第一个就移除哨兵位的下一个节点的情况)
思路: 链表的定义具有递归的性质,因此链表题目常可以用递归的方法求解。
对于给定的链表:
递归的终止条件 是 head 为空,此时直接返回 head。
当 head 不为空时,递归地进行删除操作,然后判断 head 的节点值是否等于 val 并决定是否要删除 head。
代码实现(提交已通过):
struct ListNode* removeElements(struct ListNode* head, int val)
{
if (head == NULL)
return head;
head->next = removeElements(head->next, val);
return head->val == val ? head->next : head;
}