数据结构之超硬核热门复杂度数组链表OJ题2W+文字+图片详解
Posted 小赵小赵福星高照~
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构之超硬核热门复杂度数组链表OJ题2W+文字+图片详解相关的知识,希望对你有一定的参考价值。
OJ题
文章目录
首先我们先了解一下OJ题的形式,形式有两种:
- 接口型
提供一个接口函数,而不是完整程序,实现这个函数就可以了,提交程序以后,这段代码被提交到OJ服务器上,它还要和其他测试程序(头文件、main函数)进行合并
测试用例一般是通过参数和返回值进行交互的。
- IO型
写代码区域什么都不给你,自己写完整的程序,头文件,主函数,算法逻辑
我们要去接口IO输入测试用例,要把结构按格式输出
复杂度的OJ练习
1.消失的数字
题目描述:
数组nums包含从0到n的所有整数,但其中缺了一个。请编写代码找出那个缺失的整数。你有办法在O(n)时间内完成吗?
示例 1:
输入:[3,0,1]
输出:2示例 2:
输入:[9,6,4,2,3,5,7,0,1]
输出:8
题目来源:消失的数字
解法1:
首先排序,然后依次查找上一个数+1是否等于下一个数,等于就继续,不等于这个数就是缺失的数字
但是复杂度不符合要求,qsort快排,时间复杂度O(N*logN),而题目要求是O(n),故我们看下一种解法:
解法2:
求和,循环计算出0+1+2+…+n,再减去数组中的值累加就是缺失的数字
代码如下:
int missingNumber(int* nums,int numssize)
{
int i=0;
int sum=0;
int sum1=0;
for(i=0;i<numssize;i++)
{
sum+=nums[i];
}
for(i=1;i<=numssize;i++)
{
sum1+=i;
}
return sum1-sum;
}
首先计算出数组中所有数字的和,然后再计算出0-numssize的和,相减就是消失的数字。
解法3:
看解法3的思路之前我们需要知道一个知识:两个相同的数异或等于0,0和任何数异或等于它本身
比如3异或3,不同为1,他们的二进制位都是相同的,故全是0,所以等于0
解法3思路:
创建一个变量x=0,x先跟数组中值异或,再跟与0-n之间的数异或,相当于其他数出现了两次,出现两次的都异或都成为了0,缺失的数字只出现一次,故最好x就是缺失的数字
时间复杂度O(N),满足要求
代码如下:
int missingnumber(int* nums,int numssize)
{
int x=0;
//先跟数组中值异或
for(int i=0;i<numssize;i++)
{
x^=nums[i];
}
//再跟0-n的数字异或
for(int i=0;i<=numssize;i++)
{
x^=i;
}
return x;
}
2.旋转数组
题目描述:
给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数。
进阶:
尽可能想出更多的解决方案,至少有三种不同的方法可以解决这个问题。
你可以使用空间复杂度为 O(1) 的 原地 算法解决这个问题吗?示例 1:
输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右旋转 1 步: [7,1,2,3,4,5,6]
向右旋转 2 步: [6,7,1,2,3,4,5]
向右旋转 3 步: [5,6,7,1,2,3,4]
题目来源:旋转数组
解法一:
右旋一次:保留最后一个值到temp变量,数组中值都向右移动一次,再把temp放到最左边
然后外面套一层循环,右旋k次
时间复杂度:0(N*(K%N))->最好是K==N时,时间复杂度位O(1) ,最坏是O(N^2) ,是在K==N-1时,时间复杂度考虑最坏情况,故时间复杂度为O(N^2),这个方法的时间性能不高
空间复杂度为O(1)
代码如下:
void rotate(char*str,int k)
{
int i=0;
int len=strlen(str)
for(i=0;i<k%len;i++)
{
char temp=str[len-1]
int j=0;
for(j=len-1;j>0;j--)
{
str[j]=str[j-1];
}
str[0]=temp;
}
}
这里需要考虑的是k等于len时,相当于没有旋转,故我们这里外面for循环的终止循环条件为i<k%len。
解法二:
思路:
这是空间换时间的解法,用两个数组:直接把后k个放在第二个数组的前k个上,前n-k个放在第二个数组的后面
代码如下:
int main()
{
int arr1[] = { 1,2,3,4,5,6,7 };
int arr2[10] = { 0 };
int k = 0;
scanf("%d", &k);
int sz = sizeof(arr1) / sizeof(arr1[0]);
int i = 0;
//把后k个数放在第二个数组的前k个上
for (i = 0; i < k; i++)
{
arr2[i] = arr1[sz - k + i];
}
//把前n-k个数放在第二个数组的后面
for (i = 0; i < sz - k; i++)
{
arr2[k + i] = arr1[i];
}
for (i = 0; i < sz; i++)
{
printf("%d ", arr2[i]);
}
return 0;
}
时间复杂度:O(N)
空间复杂度:O(N)
这样可以解决这个问题,但是这个OJ题是不能这样写的,因为这个OJ题是接口型的:
我们将数组一的元素放在数组二,我们没有改变数组一的旋转,只是将数组一旋转的结果放在了数组二,况且这个函数的返回值为void,我们也不能将数组二返回,所有这个思路我们有这个想法就好了,这是解决的一种方法,但是不能过而已。
下面看解法3,这个是最优的解法
解法3:三步翻转法
1、将前n-k个数逆置
2、后k个数逆置
3、整体逆置
void reverse(int *nums,int begin,int end)
{
while(begin<end)
{
int temp=nums[begin];
nums[begin]=nums[end];
nums[end]=temp;
begin++;
end--;
}
}
void rotate(int *nums,int numssize,int k)
{
k%=numssize;
reverse(nums,0,numssize-k-1);
//将前n-k个数逆置
reverse(nums,numssize-k,numssize-1);
//后k个数逆置
reverse(nums,0,numssize-1);
//整体逆置
}
这个解法的时间复杂度:O(N),空间复杂度:O(1),都是最优的,建议大家写第三个解法,代码也不难,只需写一个逆置的函数,只需要注意逆置传参时对于前n-k个和后k个的参数的把握要明确。
下面我们来看顺序表数组中的一些OJ题:
数组的相关OJ题
1.移除元素
题目描述:
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
题目来源:移除元素
原地移除数组中所有的元素val,要求时间复杂度为O(N),空间复杂度为O(1)
思路1:
遍历数组,遇到val值,将后面数据挪动一个单位删除
时间复杂度:O(N^2)
空间复杂度:O(1)
int removeElement(int* nums, int numsSize, int val)
{
int i = 0;
int temp = numsSize;
//1234546
for (i = 0; i < numsSize; i++)
{
if (nums[i] == val)
{
int begin = i;
while (begin<numsSize-1)
{
nums[begin] = nums[begin + 1];
begin++;
}
temp--;//数组当前元素个数
i--;
numsSize--;
}
}
return temp;
}
//测试代码
int main()
{
int nums[]={1,2,3,4,5,4,6};
int numsSize=sizeof(nums)/sizeof(nums[0]);
int ret = removeElement(nums,numsSize,4);
for(int i=0;i<ret;i++)
{
printf("%d ",nums[i]);
}
return 0;
}
在每次挪动数据后我们需要将i–,因为此时下标的i值为val的下标,val后面的元素前移了,此时的i下标对应的是val的下一个元素,故应该让i保持不变继续遍历,同时遍历的次数也应该少,防止越界,我们也要让numsSize–,temp来记录元素个数。
思路2:
空间换时间:把原数组中不是3的所有值,拷贝到新数组,再拷贝回来,时间复杂度O(N),空间复杂度O(N),只不过这里不满足题的要求,因为题目空间复杂度要求O(N)
我们这里能知道这个思路就可以了,这个思路是不能解这道题的,因为题目空间复杂度要求O(N),思路2测试代码如下:
int removeElement(int* nums1, int* nums2, int numsSize, int val)
{
int i = 0;
int temp = 0;
for (i = 0; i < numsSize; i++)
{
if (nums1[i] != val)
{
nums2[i] = nums1[i];
temp++;
}
else
{
nums2--;
}
}
return temp;
}
int main()
{
int nums1[] = { 1,2,3,4,5,4,6 };
int nums2[8] = {0};//1,2,3,5,6
int numsSize = sizeof(nums1) / sizeof(nums1[0]);
int ret = removeElement(nums1, nums2, numsSize, 4);
for (int i = 0; i < ret; i++)
{
printf("%d ", nums2[i]);
}
return 0;
}
下面给出最优的思路:
思路3:
给两个指针dest,src,指向起始位置,src找不是val的,放到dest指向的位置,时间复杂度:O(N),空间复杂度:O(1)
src指向数组外时停止,然后返回dest,恰好就是删除val值后的数组
代码如下:
int removeElement(int *nums,int numsSize)
{
int src=0,dest=0;
while(src<numssize)
{
if(nums[src]==val)
{
src++;
}
else
{
nums[dest]=nums[src];
src++;
dest++;
}
}
return dest;
}
2.删除有序数组中的重复值
题目描述:
给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。
不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
示例 1:
输入:nums = [1,1,2]
输出:2, nums = [1,2]
解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。示例 2:
输入:nums = [0,0,1,1,1,2,2,3,3,4]
输出:5, nums = [0,1,2,3,4]
解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。
题目来源:删除有序数组中的重复值
思路:
src找跟dest不相等的值,没找到src++,找到了,dest++,把src放入dest然后src++
代码如下:
int removeDuplicates(int* nums, int numsSize){
int src=0;
int dest=0;
if(numsSize==0)
{
return 0;
}
while(src<numsSize)
{
if(nums[src]==nums[dest])
{
src++;
}
else
{
dest++;
nums[dest]=nums[src];
src++;
}
}
return dest+1;//返回数组的长度
}
3.合并两个有序数组
题目描述:
给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。你可以假设 nums1 的空间大小等于 m + n,这样它就有足够的空间保存来自 nums2 的元素。
示例 1:
输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]
示例 2:输入:nums1 = [1], m = 1, nums2 = [], n = 0
输出:[1]
题目来源:合并两个数组
思路:
解法1:将nums2数据拷贝到nums1,qsort快排nums1,时间复杂度为(m+n)*log(m+n)
解法2:开辟一个m+n新数组,归并两个小数组,从头比较两个数组的值,把小的放到新数组,再拷贝到nums1,时间复杂度O(M+N),空间复杂度O(m+n)
解法3:依次比较取大的从后往前放到nums1里面
这里我们讲解解法3:
经过上图分析,代码如下:
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n){
int i1 = m-1,i2 = n-1;
int dest=m+n-1;
while(i1>=0 && i2>=0)//两个都大于等于0时继续
{
if(nums1[i1]>nums2[i2])//取大的放入nums1的后面
{
nums1[dest--]=nums1[i1--];
}
else
{
nums1[dest--]=nums2[i2--];
}
}
//如果是i2<0,nums2拷贝结束,那就处理结束了,因为剩下的数本来就在nums1里面
//如果是nums1结束,得把nums2的数据挪过去
while(i2>=0)
{
nums1[dest--]=nums2[i2--];
}
}
需要注意的是:
如果是i2<0,nums2拷贝结束,那就处理结束了,因为剩下的数本来就在nums1里面
如果是nums1结束,得把nums2的数据挪过去
我们OJ报错如何分析OJ程序?
-
如果没过,先习惯用它报错给的测试用例去走读代码
-
代码拷贝到vs上,补齐其他代码,用没过的测试去测试
链表OJ题
1.移除链表元素
题目描述:
题目来源:移除链表元素
思路一:把链表中所有不是val的值尾插到新链表中,删除等于val的结点
思路解析:
此外我们需要考虑传进来的head为NULL的情况,如果head为NULL,我们直接返回NULL
思路一代码如下:
struct ListNode* removeElements(struct ListNode* head, int val)
{
if(head==NULL)
{
return NULL;
}
struct ListNode*newhead=NULL,*tail=NULL;//新链表的头节点和记录尾结点
//是val删除,不是val尾插到新链表
struct ListNode*cur=head;
while(cur)
{
struct ListNode* next=cur->next;//便于找到当前结点的下一个结点,定义一个next
if(cur->val==val)//是val删除
{
free(cur);
}
else//不是val进行尾插
{
//tail是NULL时,说明没有元素
if(tail==NULL)//尾插第一个元素
{
newhead=cur;
tail=cur;
}
else
{
tail->next=cur;
tail=cur;
}
}
cur=next;
}
//当链表的最后一个结点的data是val时,前一个结点不是val时,我们将前一个结点尾插到新链表,将最后一个结点删除时,然而我们并没有将前一个结点的next置为NULL,所以这里要将其置空;并且我们这里需要判断tail是不是NULL,不是NULL我们才能进行此操作,当链表的val值都是指定删除的val值时,此时没有尾插进新链表元素,此时tail为NULL,就不能将tail的next置为NULL了
if(tail)
{
tail->next=NULL;
}
return newhead;
}
需要注意的是:
当链表的最后一个结点的data是val时,前一个结点不是val时,我们将前一个结点尾插到新链表,将最后一个结点删除时,然而我们并没有将前一个结点的next置为NULL,所以这里要将其置空;并且我们这里需要判断tail是不是NULL,不是NULL我们才能进行此操作,当链表的val值都是指定删除的val值时,此时没有尾插进新链表元素,此时tail为NULL,就不能将tail的next置为NULL了
思路二:cur找到val所在结点,prev记录前一个结点,进行链表的删除操作,链表的删除操作需要找到删除的结点的前一个结点
struct ListNode* removeElements(struct ListNode* head, int val)
{
if (head == NULL)
{
return NULL;
}
struct ListNode* cur = head;
struct ListNode* prev = head;
while (cur)
{
while ( cur && (cur->val) != val )
{
prev = cur;//记录当前结点的前一个结点
cur = cur->next;
}
//while出来有两种情况1、cur为NULL,说明走到结束了,2、(cur->val) == val,此时要删除
if(cur==NULL)
{
return head;
}
//找到val、删除
if (cur == head)//如果第一个就为val值,头删
{
struct ListNode* newhead = cur->next;
free以上是关于数据结构之超硬核热门复杂度数组链表OJ题2W+文字+图片详解的主要内容,如果未能解决你的问题,请参考以下文章
指针的这些知识你知道吗?C语言超硬核指针进阶版3w+字详解+指针笔试题画图+文字详细讲解
自定义类型的这些知识你知道吗?C语言超硬核结构体枚举联合体画图+文字详细讲解