数据结构之超硬核热门复杂度、数组、链表OJ题2W+文字+图片详解

news/2024/5/20 9:50:39 标签: OJ题, 数据结构, 链表, 顺序表, 复杂度

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个放在第二个数组的后面

image-20210809213842743

代码如下:

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题是接口型的:

image-20210809212147316

我们将数组一的元素放在数组二,我们没有改变数组一的旋转,只是将数组一旋转的结果放在了数组二,况且这个函数的返回值为void,我们也不能将数组二返回,所有这个思路我们有这个想法就好了,这是解决的一种方法,但是不能过而已。

下面看解法3,这个是最优的解法

解法3:三步翻转法

1、将前n-k个数逆置

2、后k个数逆置

3、整体逆置

image-20210809214252560

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)

删除val

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++

image-20210810111042274

代码如下:

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:

image-20210810121726627

经过上图分析,代码如下:

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.移除链表元素

题目描述:

image-20210730154059592

题目来源:移除链表元素

思路一:链表中所有不是val的值尾插到新链表中,删除等于val的结点

思路解析:

image-20210730163905994

image-20210730164641475

image-20210730165042219

image-20210730165357174

image-20210730170343903

此外我们需要考虑传进来的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记录前一个结点,进行链表的删除操作,链表的删除操作需要找到删除的结点的前一个结点

image-20210730154059592

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(cur);
            cur = newhead;
            head = newhead;
        }
        else//后面的删除
        {
            prev->next = cur->next;
            free(cur);
            cur = prev->next;
        }

    }
    return head;
}

定义一个cur和prev来进行遍历,prev记录cur的前一个结点,cur不为空进循环,然后再用一个循环来找是val值的结点,while出来有两种情况:1、cur为NULL,说明走到结束了,2、(cur->val) == val,此时要删除;所以接下来需要判断cur是不是NULL,是NULL则直接返回head,否则进行结点的删除,删除又要分头删和不是头删的情况。

2.反转链表

题目描述:

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表

image-20210731094412136

题目来源:反转链表

思路一:
调整结点指针的方向,n1和n2倒方向,n3进行迭代

<a class=链表oj" />

image-20210801115659755

当n2为NULL时,返回n1即可,但是这里有一个问题,将n2->next=n1,我们怎么找n2的下一个结点呢?这里我们还需要用一个n3结点保存之前n2的next

<a class=链表oj1" />

struct ListNode* reverseList(struct ListNode* head)
{
    if(head==NULL)
    {
        return NULL;
    }
    struct ListNode* n1,*n2,*n3;
    n1=NULL;
    n2=head;
    n3=head->next;
    
    while(n2)
    {
        n2->next=n1;
        n1=n2;
        n2=n3;
        if(n3)//当n3为NULL时,n3就没有next了
        {
        	n3=n3->next;
        }
    }
    return n1;
}

思路二:

头插法,定义一个新链表,newhead=NULL,原链表定义cur和next,next记录cur的下一个结点

struct ListNode* reverseList(struct ListNode* head)
{
    if(head==NULL)
    {
        return NULL;
    }
    struct ListNode* cur=head;
    struct ListNode* newhead=NULL;
   	struct ListNode* next=cur->next;
    
    while(cur)
    {
        next=cur->next;
        cur->next=newhead;
        newhead=cur;
        cur=next;
    }
    return newhead;
}

3.查找一个链表的中间结点

题目描述:

给定一个头结点为 head 的非空单链表,返回链表的中间结点。

如果有两个中间结点,则返回第二个中间结点。

示例 1:

输入:[1,2,3,4,5]
输出:此列表中的结点 3 (序列化形式:[3,4,5])
返回的结点值为 3 。
示例 2:

输入:[1,2,3,4,5,6]
输出:此列表中的结点 4 (序列化形式:[4,5,6])
由于该列表有两个中间结点,值分别为 3 和 4,我们返回第二个结点。

题目来源:查找一个链表的中间结点

思路:

利用快慢指针

slow一次走1步

fast一次走2步

fast走到结尾时,slow就走到了中间结点

image-20210801123119429

image-20210801123132420

image-20210801123158056

slow一次走1步,fast一次走2步,fast走到结尾时,slow就走到了中间结点

但是当时偶数个结点呢?题目的要求是返回:如果有两个中间结点,则返回第二个中间结点。

经过画图理解,发现:

image-20210801123500124

fast走到NULL时,slow到达第二个中间结点。

故循环终止的条件应为fast不为NULL并且fast->next不为NULL

解题代码如下:

struct ListNode* middleNode(struct ListNode* head)
{
    if(head==NULL)
    {
        return NULL;
    }
    //快慢指针
    struct ListNode* slow=head;
    struct ListNode* fast=head;
    while(fast&&fast->next!=NULL)
    {
        slow=slow->next;
        fast=fast->next->next;
    }
    return slow;
}

4.链表中倒数第k个结点

题目描述:

输入一个链表,输出该链表中倒数第k个结点。

示例1

输入:

1,{1,2,3,4,5}

返回值:

{5}

题目来源:查找一个链表的中间结点

思路:

和上面那个题一样,我们先定义两个指针slow,fast,fast先走k步,然后再一起走,我们看下面的动图:

<a class=链表oj2" />

当fast为NULL时,此时的slow就为倒数第k个结点

需要注意的是:输入的k如果大于结点的个数,fast还没走完k步就变为NULL了,这里要特别返回NULL

struct ListNode* FindKthToTail(struct ListNode* pListHead, int k ) 
{
    if(k==0)
    {
        return NULL;
    }
    struct ListNode*slow,*fast;
    slow=fast=pListHead;
    //fast先走k步
    while(k--)
    {
        if(fast==NULL)//输入的k大于结点的个数,fast还没走完k步就变为NULL了
        {
            return NULL;
        }
        fast=fast->next;
    }
    while(fast)
    {
        slow=slow->next;
        fast=fast->next;
    }
    return slow;
}

fast先走k步时,要是输入的k大于结点的个数,fast还没走完k步就变为NULL了,这里要特别判断一下fast是不是等于NULL,等于NULL直接返回NULL。

5.合并两个有序链表

题目描述:

将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

image-20210731124345785

思路:取小的结点下来尾插到新链表

image-20210810150402343

由思路分析,我们的代码如下:

struct ListNode* mergeTwoLists(struct ListNode* l1, struct ListNode* l2)
{
    //l1为NULL,则返回l2
    if(l1==NULL)
    {
        return l2;
    }
    //l2为NULL,则返回l1
    if(l2==NULL)
    {
        return l1;
    }
	struct ListNode*newhead=NULL;//新链表的头节点
	struct ListNode*tail=NULL;//记录新链表的尾
	struct ListNode* n1=l1;
	struct ListNode* n2=l2;
    while(n1&&n2)
    {
        //比较n1,n2的val,将小的尾插在新链表
        if(n1->val<n2->val)
        {
            //新链表为NULL时,即没有结点时
            if(tail==NULL)
            {
                newhead=tail=n1;
            }
            //不为NULL时,有结点时
            else
            {
                tail->next=n1;
                tail=n1;
            }
            n1=n1->next;//此时已经将较小的尾插进新链表,这里进行迭代
        }
        else
        {
            if(tail==NULL)
            {
                newhead=tail=n2;
            }
            else
            {
                tail->next=n2;
                tail=n2;
            }
            n2=n2->next;
        }
        //n1或者n2为NULL时结束
    }
    //将不为NULL的剩下的结点链接到新链表的尾
    if(n1)
    {
        tail->next=n1;
    }
    if(n2)
    {
        tail->next=n2;
    }
    return newhead;
}

那么我们还能不能优化一下呢?

既然不管插n1还是n2我们第一次插进新链表时都要特殊处理,那么我们可以先在外面先给它插一个结点进去,这里就不用再循环里面if和else里面都要考虑没有结点时的尾插情况了,代码如下:

struct ListNode* mergeTwoLists(struct ListNode* l1, struct ListNode* l2)
{
    if(l1==NULL)
    {
        return l2;
    }
    if(l2==NULL)
    {
        return l1;
    }
    struct ListNode*newhead=NULL;
    struct ListNode*tail=NULL;
    struct ListNode* n1=l1;
	struct ListNode* n2=l2;
    if(n1->val<n2->val)
    {
        newhead=tail=n1;
        n1=n1->next;
    }
    else
    {
        newhead=tail=n2;
        n2=n2->next;
    }
    while(n1&&n2)
    {
        if(n1->val<n2->val)
        {
            //尾插n1
            tail->next=n1;
            tail=n1;
            n1=n1->next;
        }
        else
        {
            //尾插n2
            tail->next=n2;
            tail=n2;
            n2=n2->next;
        }
    }
    if(n1)
    {
        tail->next=n1;
    }
    if(n2)
    {
        tail->next=n2;
    }
    return newhead;
}

还有一种方法不需要考虑尾插时新链表为NULL的情况,那就是我们设置一个带哨兵位的头结点,下面我们介绍一下:

image-20210802084756149

带哨兵位的头节点,这个结点不存储有效数据,函数永远不会修改到指向存储有效数据的第一个结点的指针plist,因为拥有这个带哨兵位的头节点,对在第一元素结点前插入结点和删除第一结点时,其操作与其他结点的操作就统一了。

这个带哨兵位的头节点看起来简单一点,为什么我们的单链表不设计这个结构呢?

这个头节点的结构在实际中很少出现,包括哈希桶、邻接表做子结构都是不带头,其次就是OJ中给的链表基本都是不带头的。

带哨兵位的头节点方法:

struct ListNode* mergeTwoLists(struct ListNode* l1, struct ListNode* l2)
{
    if(l1==NULL)
    {
        return l2;
    }
    if(l2==NULL)
    {
        return l1;
    }
    struct ListNode*newhead=NULL;
    struct ListNode*tail=NULL;
    newhead=tail=(struct ListNode*)malloc(sizeof(struct ListNode));
    while(l1&&l2)
    {
        if(l1->val<l2->val)
        {
            //尾插l1
            tail->next=l1;
            tail=l1;
            l1=l1->next;
        }
        else
        {
            //尾插l2
            tail->next=l2;
            tail=l2;
            l2=l2->next;
        }
    }
    if(l1)
    {
        tail->next=l1;
    }
    if(l2)
    {
        tail->next=l2;
    }
	
    //return newhead->next;直接return这个会造成内存泄漏
    struct ListNode* first = newhead->next;//将newhead的next先保存,然后将newhead free
    free(newhead);
    return first;
}

需要注意的是,我们最后要将这个开辟的带哨兵位的头结点需要释放掉,否则会有内存泄漏,我们需要返回newhead的next,那么我们先用first将newhead的next先保存,然后将newhead free掉

6.链表分割

题目描述:

现有一链表的头指针 ListNode pHead,给一定值x,编写一段代码将所有小于x的结点排在其余结点之前,且不能改变原来的数据顺序,返回重新排列后的链表的头指针。*

思路:

1、把比x小的尾插入一个链表

2、把比x大的尾插入一个链表

3、再把两个链表链接在一起

image-20210810172015777

代码如下:

ListNode* partition(ListNode* pHead, int x) 
{
        // write code here
        struct ListNode* LessHead=(struct ListNode*)malloc(sizeof(struct ListNode));//较小值的带哨兵位头节点
        struct ListNode* GreaterHead=(struct ListNode*)malloc(sizeof(struct ListNode));//较大值的带哨兵位头节点
        struct ListNode* LessTail=LessHead;
        struct ListNode* GreaterTail=GreaterHead;
        struct ListNode* cur = pHead;
        while(cur)
        {
            if(cur->val<x)
            {
                //尾插到存放较小值的链表
                LessTail->next=cur;
                LessTail=cur;
            }
            else
            {
                //尾插到存放较大值的链表
                
                GreaterTail->next=cur;
                GreaterTail=cur;
            }
            cur=cur->next;
        }        
        LessTail->next=GreaterHead->next;//链接
        GreaterTail->next=NULL;
        
        struct ListNode* first=LessHead->next;
        free(LessHead);
        LessHead=NULL;
        free(GreaterHead);
        GreaterHead=NULL;
        
        return first;
}

7.链表的回文结构

题目描述:

对于一个链表,请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法,判断其是否为回文结构。

给定一个链表的头指针A,请返回一个bool值,代表其是否为回文结构。保证链表长度小于等于900。

测试样例:

1->2->2->1
返回:true

1->2->3->2->1
返回:true

题目来源:链表的回文结构

思路1:

定义一个数组将链表中的val值都拷贝进去,然后通过下标去比较第一个和最后一个是不是相等

bool chkPalindrome(ListNode* A) {
        // write code here
        
        int a[900];
        struct ListNode* cur=A;
        int n=0;
        while(cur)
        {
            a[n++]=cur->val;
            cur=cur->next;
        }
        int left=0;
        int right=n-1;
        while(left<right)
        {
            if(a[left]!=a[right])
            {
                return false;
            }
            left++;
            right--;
        }
        return true;
    }

这种解法空间复杂度为O(n),空间复杂度不符合要求,但是牛客网给过了,有些OJ题目,有时间复杂度和空间复杂度的要求,但是后台测试用例的检查对于时间复杂度和空间复杂度是不好检查的,对于复杂度,OJ系统通常检查的都不是很严格。这一块leetcode会严格一些,牛客会更松散,包括测试用例完整度,也是这样的。

下面我们看一种正确的解法:

思路2:

1、先找到中间结点

2、将后半段逆置

3、比较前半段和后半段

找中间结点和逆置链表我们前面已经讲过了,剩下的就是比较前半段和后半段了

image-20210810182133356

代码如下:

struct ListNode* FindMidNode(struct ListNode* phead)//找到中间结点
{
    struct ListNode*slow,fast;
    slow=fast=phead;
    while(fast&&fast->next)
    {
        slow=slow->next;
        fast=fast->next;
    }
    return slow;
}

//传一级指针,用返回值返回头指针
/*struct ListNode* reverseList(struct ListNode* phead)//逆置链表
{
    struct ListNode*cur=phead;
    struct ListNode*newhead=NULL;
    while(cur)
    {
        struct ListNode*next=cur->next;
        cur->next=newhead;
        newhead=cur;
        cur=next;
    }
    return newhead;
}*/

//传二级指针,在函数里面修改头指针
void reverseList(struct ListNode** pphead)
{   
    struct ListNode*cur=pphead;
    struct ListNode*newhead=NULL;
    while(cur)//头插法逆置
    {
        struct ListNode*next=cur->next;
        cur->next=newhead;
        newhead=cur;
        cur=next;
    }
    *pphead = newhead;

bool chkPalindrome(ListNode* A) 
{
    struct ListNode*mid = FindMidNode(A);
    //struct ListNode*rHead = reverseList(mid);
    struct ListNode*rhead=mid;
    reverseList(&rhead);
    struct ListNode*head=A;
    while(rhead && head)
    {
        if(rhead->val!=head->val)
        	return false;
        
        rhead=rhead->next;
        head=head->next;
    }
    return true;
}

8.链表的相交

题目描述:

给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。

图示两个链表在节点 c1 开始相交:

img

题目数据 保证 整个链式结构中不存在环。

注意,函数返回结果后,链表必须 保持其原始结构

题目来源:链表的相交

如何判断A、B链表是否相交?

思路1:

常规思路:A链表的每个结点依次和b链表的所有结点比较,如果有相等,就是交点,时间复杂度O(N^2)

优化思路:分别算出A和B的长度,lenA和lenB,让长的先走|lenA-lenB|步,再比较找交点,时间复杂度O(N)

思路2:判断最后一个结点是否相同

题目要求我们返回相交的结点,所以我们用思路1的优化思路来解这道题:

image-20210810190808021

代码如下:

struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    struct ListNode *curA = headA;
    struct ListNode *curB = headB;
    int lenB=0;
    int lenA=0;
    while(curA)//计算A的长度
    {
        lenA++;
        curA=curA->next;
    }
    while(curB)//计算B的长度
    {
        lenB++;
        curB=curB->next;
    }
    //假设A长B短
    struct ListNode *longList=headA;
    struct ListNode *shortList=headB;
    if(lenA<lenB)
    {
        longList=headB;
        shortList=headA;
    }

    //让长的先走差距步
    int gap=abs(lenA-lenB);
    while(gap--)
    {
        longList=longList->next;
    }
    //同时走,找交点
    while(longList && shortList)
    {
        if(longList == shortList)
        {
            return longList;//找到了返回
        }
        longList=longList->next;
        shortList=shortList->next;
    }

    //表示不相交
    return NULL;
}

首先我们计算出A、B链表的长度,然后我们先假设长的链表为A,短的是B,然后如果lenA<lenB的话,再将B赋给长链表,A赋给短链表,然后我们让长的先走差距步,最后一起走然后找交点

9.环形链表

问题描述:

给定一个链表,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

如果链表中存在环,则返回 true 。 否则,返回 false 。

进阶:

你能用 O(1)(即,常量)内存解决此问题吗?
示例 1:

img

输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:

img

输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:

img

输入:head = [1], pos = -1
输出:false
解释:链表中没有环。

题目来源:环形链表

思路:

快慢指针,慢指针一次走一步。快指针一次走两步,如果快指针能等于慢指针,则是环形链表,否则则不是

image-20210810211447108

代码如下:

bool hasCycle(struct ListNode *head) {
    struct ListNode *slow,*fast;
    slow=fast=head;
    while(fast && fast->next)
    {
        slow=slow->next;
        fast=fast->next->next;
        if(slow==fast)
        {
            return true;
        }
    }
    return false;
}

这道题代码很简单,但是考面试中考察这道题可能不止会让你写出代码就完事了,面试官可能会问:

面试提问:

  • slow一次走一步,fast一次走两步?一定可以追上吗?请证明:

解答:

image-20210803171453208

一定可以追上,slow进环时,fast已经在环里面走了一定长度了,那么这时在环里面就形成追赶,fast去追slow,假设slow进环时,fast和slow之间的距离为N,环的大小为C,那么N一定小于C

追赶中,slow一次走1步,fast一次走两步

fast和slow之间的距离变化是:

N,N-1,N-2,N-3…2,1,0

N最后一定会被缩小到0,那么距离为0就一定相遇了

slow进入环里面,一圈之内,fast一定追上slow

  • slow一次走一步,fast一次走n步,n>(2,3,4,5…),可以吗?首先环的大小,和环前面的长度,都是不确定的,请证明:

解答:

不一定能追上

假设slow一次走1步,fast一次走3步,slow进环时,它们之间差距是N,环的长度是C

fast和slow之间的距离变化如下:

N,N-2,N-4,…,

slow和fast之间的距离是0的时候就追上了,那么说明N是偶数,就一定能追上

假设N是奇数,C-1也是奇数,就永远追不上

fast追到距离为-1时,此时它们的距离为C-1,C-1也是奇数时,就永远追不上

总结:fast一次走x,slow一次走y步都是可以的,最重要的是,追赶过程中的步差为x-y,步差为1就一定能追上,其他不一定能追上

10.环形链表

题目描述:

给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。

说明:不允许修改给定的链表

进阶:

你是否可以使用 O(1) 空间解决此题?

题目来源:环形链表

解题思路1:

一个指针从相遇点开始走,一个指针从head走,它们会在入口点相遇

image-20210810225036010

证明:

假设:链表头到入口点的距离是L,相遇点到入口点的距离是X,环的长度为C,

慢指针走的路程是L+X

快指针走的路程是L+C+X

2(L+X)=L+C+X得L=C-X*

很多文章的证明是这个,但是这个证明是错的,因为slow进环之前,fast不一定在环里面只走了一圈

正确的证明

相遇时:慢指针走的路程L+X

快指针走的路程L+NC+X(N>=1)

2(L+X)=L+NC+X

L+X=NC

L=NC-X

L=(N-1)C+C-X

结论:一个指针从相遇点开始走,一个指针从head走,它们会在入口点相遇

解题思路2:

断开meet点,然后判断相交,返回交点

image-20210810230151612

代码如下:

 struct ListNode* getIntersectionNode(struct ListNode* headA, struct ListNode* headB) {
        //1、计算A、B长度
        struct ListNode*curA = headA;
        struct ListNode*curB = headB;
        int lenA=0;
        int lenB=0;
        while(curA)
        {
            lenA++;
            curA=curA->next;
        }
        while(curB)
        {
            lenB++;
            curB=curB->next;
        }
        //2、计算长度差值,长的先走差值步
        int gap = abs(lenA-lenB);
        struct ListNode* longList = headA;
        struct ListNode* shortList = headB; 
        if(lenA<lenB)
        {
            longList = headB;
            shortList = headA; 
        }
        while(gap--)
        {
            longList=longList->next;
        }
        //3、然后一起走
        while(longList && shortList)
        {
            if(longList==shortList)
            {
                return longList;
            }
            longList=longList->next;
            shortList=shortList->next;
        }
        return NULL;
}

struct ListNode *detectCycle(struct ListNode *head) {
    struct ListNode *slow=head;
    struct ListNode *fast=head;
    while(fast &&fast->next)
    {
        slow=slow->next;
        fast=fast->next->next;
        if(slow==fast)
        {
            //相遇了
            struct ListNode *meet=slow;
            struct ListNode *next=meet->next;//记录相遇点的下一个节点
            meet->next=NULL;//断开相遇点
            return getIntersectionNode(head,next);
        }
    }
    return NULL;
}

记录meet的next,然后从next开始和head开始判断是否相交,相交的话返回交点,回到了我们之前讲的相交链表那个题

11.复制带随机指针的链表

题目描述:

给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。

构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。

例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random --> y 。

返回复制链表的头节点。
链接:https://leetcode-cn.com/problems/copy-list-with-random-pointer

示例 1

img

输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]

示例 2:

img

输入:head = [[1,1],[2,1]]
输出:[[1,1],[2,1]]

示例 3:

img

输入:head = [[3,null],[3,0],[3,null]]
输出:[[3,null],[3,0],[3,null]]

示例 4:

输入:head = []
输出:[]
解释:给定的链表为空(空指针),因此返回 null。

题目来源:复制带随机指针的链表

思路1:

1、先复制原链表的每个节点,将复制好的新节点链接起来

2、求出原链表中每个结点cur的random和cur的相对距离

再去复制新链表到当前节点相对距离的结点,赋值给random,找每个节点的random时间复杂度是O(N),N个节点就是O(N^2)——效率低

思路2:

1、在原链表的每个节点的后面,链接插入一个拷贝节点

2、置random

3、把拷贝节点剪下来,链接到一起,同时恢复原链表

image-20210811120733969

代码如下:

struct Node* copyRandomList(struct Node* head) {
    if(head==NULL)
    {
        return NULL;
    }
	//1、每个节点的拷贝链接在该节点后面
    struct Node* cur=head;
    while(cur)
    {
        struct Node* next = cur->next;
        struct Node* copy = (struct Node*)malloc(sizeof(struct Node));
        copy->val=cur->val;
        cur->next=copy;
        copy->next=next;
        cur=next;
    }
    //2、置random
    cur=head;
    while(cur)
    {
        struct Node* copy=cur->next;
        if(cur->random)
        {
            copy->random = cur->random->next;
        }
        else
        {
            copy->random=NULL;
        }
        cur=copy->next;
    }
    //3、剪切拷贝的节点,尾插到新链表
    cur=head;
    struct Node* newhead,*tail;
    newhead=tail=(struct Node*)malloc(sizeof(struct Node));
    while(cur)
    {
        struct Node* copy=cur->next;
        struct Node* next=copy->next;
        tail->next=copy;
        tail=copy;

        cur->next=next;
        cur=next;
    }    
    struct Node* first=newhead->next;
    free(newhead);
    newhead=NULL;
    return first;
}

将每个节点的拷贝链接在该节点后面相当于链表当中的插入操作,每个拷贝结点都在原结点的后面,原结点和拷贝结点就建立了一个相对关系,我们可以发现我们很容易就能找到拷贝结点的random在哪,他就是cur->random->next,然后通过相对位置置拷贝的结点random,最后将拷贝的结点尾插到新链表,这里我们对新链表使用了带哨兵位的头结点。

以上就是博主关于复杂度、数组、链表的热门OJ的讲解,期待你的点赞哦!欢迎大家学习讨论,如有错误,希望大家评论区留言。


http://www.niftyadmin.cn/n/880339.html

相关文章

暑假第一天之每天一些题系列

暑假第一天之每天一些题系列 文章目录暑假第一天之每天一些题系列一、选择题二、填空题三、算法题一、选择题 若有定义&#xff1a; int a[] {2,4,6,8,10,12,14,16,18,20,22,24},*q[4],k; for(k0; k<4; k) {q[k] &a[k*3]; } printf("%d\n",q[3][1]);则上面…

暑假第二天之每天一些题系列

暑假第二天之每天一些题系列 文章目录暑假第二天之每天一些题系列一、选择题二、填空题三、算法题一、选择题 表达式 0x13&0x17,0x13|0x17 的值分别是多少 A. 0x17 0x13 B. 0x13 0x17 C. 0xF8 0xE8 D. 0xec 0xC8 答案解析&#xff1a; 0x13的二进制为&#xff1a;00010011…

暑假第三天之每天一些题系列

暑假第三天之每天一些题系列 文章目录暑假第三天之每天一些题系列一. 选择题二、填空题三、算法题一. 选择题 以下关于函数设计不正确的说法是 A. 函数设计应该追求高内聚低耦合 B. 要尽可能多的使用全局变量 C. 函数参数不易过多 D. 设计函数时&#xff0c;尽量做到谁申请的资…

暑假第四天之每天一些题系列

暑假第四天之每天一些题系列 文章目录暑假第四天之每天一些题系列一、选择题二、填空题三、算法题一、选择题 下列程序执行后&#xff0c; n 的值等于 char a[20]; char *p1 (char *)a; char *p2 (char*)(a5); int n p2-p1;A. 4 B. 5 C. 10 D. 20 答案解析&#xff1a; 数…

暑假第五天之每天一些题系列

暑假第五天之每天一些题系列 一、选择题 如下程序: int a[10]; int*pa; pa a;则元素a[1]的地址可以表示为 A. pa1 B. pa2 C. pa4 D. a2 答案解析&#xff1a; 数字名a是首元素的地址&#xff0c;pa存放的是首元素的地址&#xff0c;pa1是第二个元素的地址&#xff0c;即a[1…

暑假第六天之每天一些题系列

暑假第六天之每天一些题系列 一、选择题 以下程序的运行结果是 int fun(int a,int b) {if(a>b)return(ab);elsereturn(a-b); } int main() {int x 3, y 8, z 6, r;r fun (fun(x,y), 2 * z);printf(“%d\n”,r);return 0; }A. -48 B. 58 C. -58 D. -17 答案解析&#xf…

暑假第七天之每天一些题系列

暑假第七天之每天一些题系列 一、选择题 设 m 和 n 都是 int 类型&#xff0c;那么以下 for 循环语句 for(m0,n-1; n0; m,n) n;A. 循环体一次也不执行 B. 循环体执行一次 C. 是无限循环 D. 有限次循环 E. 循环结束判断条件不合法 F. 运行出错 答案解析&#xff1a; 循环进行…

暑假第八天之每天一些题系列

暑假第八天之每天一些题系列 一、选择题 若有定义 typedef char STRING[255]; STRING s; 则 s 是 A. 字符指针数组变量 B. 字符数组变量 C. 字符变量 D. 字符指针变量 答案解析&#xff1a; typedef char STRING[255];将char类型重定义为STRING[255]&#xff0c;而STRING s就…