数据结构之超硬核热门复杂度数组链表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语言超硬核结构体枚举联合体画图+文字详细讲解

数据结构学习笔记(数组链表OJ题)整理与总结

数据结构学习笔记(数组链表OJ题)整理与总结

❤️爆肝新一代大数据存储宠儿,梳理了2万字 “超硬核” 文章!❤️

❤️爆肝新一代大数据存储宠儿,梳理了2万字 “超硬核” 文章!❤️