代码随想录 PART1(数组&&链表)

发布于:2022-11-27 ⋅ 阅读:(270) ⋅ 点赞:(0)

代码随想录

数组

1.找单身狗

【题目】:一个数组中只有两个数字是出现一次,其他所有数字都出现了两次;编写一个函数找出这两个只出现一次的数字

例如:1 2 3 4 5 1 2 3 4 6
发现5和6只出现一次

思路1:遍历统计

思路2:异或

引入:

一个数组中只有一个”单身狗“的情况:全部异或,得到的即是那个单独的数字

int main()
{
    int arr[] = {5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 6, 9};
    //所有数字异或
    int i = 0;
    int sz = sizeof(arr) / sizeof(arr[0]);
    int ret = 0;
    for (i = 0; i < sz; i++)
    {
        ret ^= arr[i];
    }
    // ret 就是5^6的结果,二进制中一定有1
    //计算ret的第几位是1
    int pos = 0;
    for (i = 0; i < 32; i++)
    {
        if (((ret >> i) & 1) == 1)
        {
            pos = i;
            break;
        }
    }
    // ret的第pos位是1
    //把arr数组中的每个元素的第pos位为1的数字异或在一起
    int num1 = 0;
    int num2 = 0;

    for (i = 0; i < sz; i++)
    {
        if (((arr[i] >> pos) & 1) == 1)
        {
            num1 ^= arr[i];
        }
        else
        {
            num2 ^= arr[i];
        }
    }
    printf("%d %d\n", num1, num2);
    return 0;
}

2.轮转数组

image-20221027140729775

思路1:

tmp变量保存最后一个变量,整体将数据向后移动一次,再将tmp赋值给第一个元素;将前面步骤重复k次

问题:效率超时;时间复杂度为O(N^2) 空间复杂度为O(1)

#include<stdio.h>
#include<string.h>
//该程序的空间复杂度大了
int main() {
	int nums[7] = { 1, 2, 3, 4, 5, 6, 7 };
	int k;
	scanf("%d", &k);
	int tmp = 0;
	int sz = sizeof(nums) / sizeof(int);
	for (int i = 0; i < k; i++)
	{
		tmp = nums[sz - 1];
		for (int j = sz - 1; j > 0; j--)
		{
			nums[j] = nums[j - 1];
		}
		nums[0] = tmp;
	}
	for (int i = 0; i < sz; i++)
	{
		printf("%d", nums[i]);
	}
}

思路2:

用空间来换取时间,开辟一个新的数组来保存移动后的元素

#include<stdio.h>
#include<string.h>
//用空间来换时间
int main() {
	int nums[7] = { 1, 2, 3, 4, 5, 6, 7 };
	int k;
	scanf("%d", &k);
	int tmp[7] = { 0 };
	int sz = sizeof(nums) / sizeof(int);
	for (int i = 0; i < sz; i++)
	{
		tmp[(i + k) % 7] = nums[i];
	}
	for (int i = 0; i < sz; i++)
	{
		nums[i] = tmp[i];
	}
	for (int i = 0; i < sz; i++)
	{
		printf("%d", nums[i]);
	}
}

思路3:

三步翻转法

1,2,3,4,5,6,7
前n-k个逆置   4,3,2,1
后k个逆置     7,6,5
整体再逆置    5,6,7,1,2,3,4
#include <stdio.h>
void reverse(int *arr, int begin, int end)
{
    while (begin < end)
    {
        int tmp = arr[begin];
        arr[begin] = arr[end];
        arr[end] = tmp;
        begin++;
        end--;
    }
}

void rotate(int *nums, int numsSize, int k)
{
    k %= numsSize;
    reverse(nums, 0, numsSize - k - 1);        //交换前numsSize-k个
    reverse(nums, numsSize - k, numsSize - 1); //交换后k个
    reverse(nums, 0, numsSize - 1);            //再整体交换
}

3.移除元素

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YpVtFB8j-1668401878558)(E:/Typora图片/image-20221027185702799.png)]

对于数组中的元素,只能做到覆盖,并不能真正实现删除

思路1:暴力实现

两层for循环,一层用来遍历数组找到目标(待删除)的值,下一层用后续元素将这个元素覆盖

这里不够优秀的地方即是:存在多次重复挪动

void move(int *nums,int sz,int begin){
    for (int i = begin; i < sz - 1;i++){
        nums[i] = nums[i + 1];
    }
}
int removeElement(int* nums, int numsSize, int val){
    for (int i = 0; i < numsSize;i++){
        if(nums[i]==val){
            move(nums, numsSize, i);
            numsSize--;
            i--;
        }
    }
    return numsSize;
}

思路2:双指针思路

定义两个指针,一个指针src指向原数组,另一指针des指向新数组tmp,src遍历原数组,值不等于val则将其赋值给新数组

#include <stdio.h>
int main()
{
    int nums[10] = {1, 2, 3, 2, 5, 6, 2, 7, 8, 2};
    int tmp[10] = {0};
    int val = 2;
    int des, sz = 0;
    for (int i = 0; i < 10; i++)
    {
        if (nums[i] != val)
        {
            tmp[des] = nums[i];
            des++;
            sz++;
        }
    }
    for (int i = 0; i < sz; i++)
    {
        nums[i] = tmp[i];
    }
    for (int i = 0; i < sz; i++)
    {
        printf("%d ", nums[i]);
    }
    return 0;
}

思路3:双指针之快慢指针

两个指针指向数组:src和des,src遍历数组来寻找与val不同的元素,若src遇到与val相同的,则指向下一个;des从下标0开始,由于src位置始终大于等于des,因此将src指向的不同于val的元素直接赋值给des指向的位置,遍历完后,des之前的元素全是与val不同的了,因此实现了删除的目的

int removeElement(int *nums, int numsSize, int val)
{
    int des = 0, src = 0;
    while (src < numsSize)
    {
        if (nums[src] == val)
        {
            src++;
        }
        else
        {
            nums[des] = nums[src];
            src++;
            des++;
        }
    }
    return des;
}

4. 删除有序数组中的重复项

image-20221030163918991

image-20221030163943542

本质就是去重(一般的,去重算法要求数组是有序的,不然效率很低)

双指针法:

int removeDuplicates(int* nums, int numsSize){
    int slow = 0;
    int quick = 0;
    while(quick<numsSize){
        if(nums[quick]==nums[slow]){
            quick++;
        }
        else{
            slow++;
            nums[slow] = nums[quick];
        }
    }
    return slow+1;
}

5.合并两个有序数组

image-20221030170359108

创建临时数组:

void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n){
    int i=0,j=0,k=0;
    int* temp = (int*)malloc(sizeof(int)*(m+n)); //创建新数组
    /* 每次都取两数组中较小值放入新表 */
    while(i<m && j<n) {
        if(nums1[i]<=nums2[j])
            temp[k++] = nums1[i++];
        else
            temp[k++] = nums2[j++];
    }
    /* 若有剩余未遍历的数组元素,说明都不比原来的值小,因此整体放入数组尾部 */
    while(i<m)
        temp[k++] = nums1[i++];
    while(j<n)
        temp[k++] = nums2[j++];

    /* 最后一步将新数组元素逐个放入nums1数组中,交差完事 */
    for(int c=0; c<m+n; c++) {
        nums1[c] = temp[c];
    }
}

6.检查两个字符串数组是否相等

image-20221101110348912

本题的关键即是考虑如何将两个字符串拼接成一个字符串的问题

而数组的本质即是一个指针,而数组里装的字符串,那么我们在对字符串处理的时候,又嵌套了一层指针,因此这是一个二级指针

而二级指针,一个指向每个字符串的首元素,而另一个则是对其中的字符串做处理

img


char * join(char ** word, int wordSize) {
    char *str = (char *)malloc(sizeof(char) * wordSize * MAX_STR_LEN + 1);
    int pos = 0;
    for (int i = 0; i < wordSize; i++) {
        pos += sprintf(str + pos, "%s", word[i]);
    }
    return str;
}

bool arrayStringsAreEqual(char ** word1, int word1Size, char ** word2, int word2Size) {
    char *str1 = join(word1, word1Size);
    char *str2 = join(word2, word2Size);
    bool ret = strcmp(str1, str2) == 0;
    free(str1);
    free(str2);
    return ret;
}

7.最大重复子字符串

image-20221103183218592

思路:kmp算法

链表

1.移除单链表的元素

img

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */


struct ListNode* removeElements(struct ListNode* head, int val){
	//如果这个单链表为空
    if(head==NULL){
        return NULL; 
    }
    struct ListNode* cur=head;          //用于指向原链表的指针
    struct ListNode* newhead=NULL;      //用于存头指针地址
    struct ListNode* tail=NULL;         //用于尾插(尾指针)(新链表)
    while(cur!=NULL){
        if(cur->val!=val){
            //如果不等于val,就把该结点尾插入新链表中
            //如果新链表为空的尾插记得单独处理
            if(tail==NULL){
                newhead=tail=cur;
            }
            else{
                tail->next=cur;
                tail=tail->next;
            }
            cur=cur->next;
        }
        else{
            //相等的情况要删除这个结点(记得保存下一个)
            struct ListNode* nextnode=cur->next;
            free(cur);
            cur=nextnode;
        }
    }
    if(tail){
        tail->next=NULL;
    }
    return newhead;
}

2.合并两个有序链表

image-20221106145328873

思路:和第七题的方法相似,这里取两个链表中小的数据来尾插

开始时,开始给一个head和一个tail置空(来操作新链表),那么对于原来的链表,即用list1和list2来指向它们的头节点;当任意的一个链表全部被插入到新链表中后,那么另一个链表的剩余部分即可直接链接入新链表后;还有一个点需要考虑的是,如果有一个链表为空,那么合并之后的链表就是另一个链表

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */


struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){
	struct ListNode* head=NULL;
    struct ListNode* tail=NULL;
    //考虑链表为空的情况
    if(list1==NULL){
        return list2;
    }
    if(list2==NULL){
        return list1;
    }
    
    while(list1&&list2){
        if((list1->val)<(list2->val)){
            if(tail==NULL){
                head=tail=list1;
            }
            else{
                tail->next=list1;
                tail=tail->next;
            }
            list1=list1->next;
        }
        else{
            if(tail==NULL){
                head=tail=list2;
            }
            else{
                tail->next=list2;
                tail=tail->next;
            }
            list2=list2->next;
        }
    }
    if(list1){
        tail->next=list1;
    }
    if(list2){
        tail->next=list2;
    }
    return head;
}

如果有一个带哨兵位的头节点,那么就不用再判断头节点为空的情况了,只需要作尾插即可

(带哨兵位的头结点,在尾插的时候比较方便)

3.反转链表

image-20221106154314464

思路,一般的,我们同样能使用尾插的思路,从尾结点开始,一个一个的尾插入新的链表之中,这里我们采用一种,新的思路:修改每个结点间指针的指向位置,让每个结点的next,指向它的前一个结点,那么首结点的next,即指向NULL

有了这个思路,我们就可以创建指针了,首先需要n1和n2指向两个结点,修改它们间的指针指向关系;然后我们,当修改next域之后,我们就无法找到下一个待修改next的指针了,因此还需要第三个指针用于储存next域(指向下一个待修改的结点)

image-20221106160749726

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */


struct ListNode* reverseList(struct ListNode* head){
    //对空表单独处理
    if(head==NULL){
        return NULL;
    }
	struct ListNode* n1=NULL;
    struct ListNode* n2=head;
    struct ListNode* n3=head->next;
    while(n2){
        n2->next=n1;
        n1=n2;
        n2=n3;
        if(n3)
        n3=n3->next;
    }
    return n1;
}

4.链表的中间结点

image-20221106160837862

要求:只能遍历一遍

思路:快慢指针

分析:定义一个slow和fast指针指向开始位置,由于两个指针移动速度差为2倍,因此在fast指向最后一个结点时,slow指针刚好指向中间结点;注意:本题需要考虑结点数是奇数个还是偶数个(指针移动的结束条件不同)

注:偶数个结点找后一个

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */

struct ListNode* middleNode(struct ListNode* head){
	struct ListNode* fast;
    struct ListNode* slow;
    fast=slow=head;
    //涉及到奇数个和偶数个结点间的差异
    while(fast && fast->next){
        slow=slow->next;
        fast=fast->next->next;
    }
    return slow;
}

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

思路:快慢指针法

由于要找到倒数第k个结点,我们知道倒数第k个结点和倒数第一个结点间差k-1个结点,因此我们同样的可以定义两个指针,slow指针指向头节点,而fast指针移动k-1步,然后再让两指针同时移动,当fast指向尾结点时,slow指向的即是倒数第k个结点/或者是让指针移动k步

如果k的值大于链表的结点个数,那么倒数第k个返回NULL

/**
 * struct ListNode {
 *	int val;
 *	struct ListNode *next;
 * };
 */

/**
 * 
 * @param pListHead ListNode类 
 * @param k int整型 
 * @return ListNode类
 */
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k ) {
    struct ListNode* fast;
    struct ListNode* slow;
    fast=slow=pListHead;
    //移动k步
    for(int i=0;i<k;i++){
        //链表可能没有k步长
        if(fast==NULL){
            return NULL;
        }
        fast=fast->next;
    }
    while(fast){
        fast=fast->next;
        slow=slow->next;
    }
    return slow;
}

6.链表分割

image-20221106171131947

思路:创建两个链表,将值小于x的尾插入一个链表,将值大于x的尾插入第二个链表,最后将两个链表合并起来即可

这道题我们采用带哨兵位的头节点,方便处理:①对空表的插入 ②防止链表的值都大于或者小于值x导致的某个链表为空的情况(对空指针的问题是一个很好的解决方案)

因此对于我们创建了两个新的链表,每个链表分别需要一个指针来保存其头节点的位置,一个指针负责移动,对链表进行操作

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};*/
class Partition {
public:
    ListNode* partition(ListNode* pHead, int x) {
        struct ListNode* lessHead;
        struct ListNode* lessTail;
        struct ListNode* greaterHead;
        struct ListNode* greaterTail;
        //创建带哨兵位的头结点
        lessHead=lessTail=(struct ListNode*)malloc(sizeof(struct ListNode));
        greaterHead=greaterTail=(struct ListNode*)malloc(sizeof(struct ListNode));
        
        struct ListNode* cur=pHead;
        while(cur)
        {
            if(cur->val<x){
                lessTail->next=cur;
                lessTail=lessTail->next;
            }
            else{
                greaterTail->next=cur;
                greaterTail=greaterTail->next;
            }
            cur=cur->next;
        }
        //合并
        lessTail->next=greaterHead->next;
        //将尾结点的next域置空
        greaterTail->next=NULL;
        
        pHead =lessHead->next;
        free(lessHead);
        free(greaterHead);
        
        return pHead;
    }
};

总结:使用尾插法时,本质是改变当前结点的next域,让下一结点作为这个结点的下一个结点,然而在最后,我们没有修改最后插入结点的next域,因此可能会引起一些问题,所以在使用尾插法时要记得考虑这一点,将最后插入的结点的next域置空

7.链表的回文结构

image-20221106185812175

思路1:

将链表反转与原链表作比较;空间复杂度为O(n)

思路2:

反转后半段,与前半段作比较(这里就要用到之前实现过的:【找到中间结点】、【反转链表】)的函数

对于偶数个:可以直接分成两段作比较;而对于奇数个:(画图很显然)——指向空就结束

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};*/
class PalindromeList
{
    //反转链表
    struct ListNode *reverseList(struct ListNode *head)
    {
        //对空表单独处理
        if (head == NULL)
        {
            return NULL;
        }
        struct ListNode *n1 = NULL;
        struct ListNode *n2 = head;
        struct ListNode *n3 = head->next;
        while (n2)
        {
            n2->next = n1;
            n1 = n2;
            n2 = n3;
            if (n3)
                n3 = n3->next;
        }
        return n1;
    }
    //找到链表中间结点
    struct ListNode *middleNode(struct ListNode *head)
    {
        struct ListNode *fast;
        struct ListNode *slow;
        fast = slow = head;
        //涉及到奇数个和偶数个结点间的差异
        while (fast && fast->next)
        {
            slow = slow->next;
            fast = fast->next->next;
        }
        return slow;
    }

public:
    bool chkPalindrome(ListNode *A)
    {
        struct ListNode *mid = middleNode(A);
        struct ListNode *rhead = reverseList(mid);
        //中间结点前的部分与中间结点后反转后的部分作比较
        while (A && rhead)
        {
            if (A->val != rhead->val)
            {
                return false;
            }
            A = A->next;
            rhead = rhead->next;
        }
        return true;
    }
};

8.相交链表

image-20221107112847898

大思路:

结点相交——同一个结点——同一个地址

思路1:

尾结点地址相同则相交:无法找到相交位置;但是使用此方法可以直接判断出不相交的情况!

思路2:

两个链表中,每个结点地址互相比较 时间复杂度为O(n^2)

思路3:

如果两个链表一样长,那么只用比较对应位置是否相等即可,时间复杂度为O(n)

注意:本题不能用逆置的思想,因为每个结点只能有一个next域

那么怎么要它们一样长呢?——仍然采用快慢指针法

对于两个链表有长有短,那么我们通过遍历可以得到两个链表间的长度关系,让长的链表先走差距步,那么此时,两个指针指向的起始位置之后的长度即相等了

curA和curB指针分别指向这两个链表

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    struct ListNode* curA=headA;
    struct ListNode* curB=headB;
    //代表A、B链表的长度
    int lenA=0;
    int lenB=0;
    
    //比较尾结点地址可直接排除不相交情况,因此这里寻找尾结点
    while(curA->next){
        lenA++;
        curA=curA->next;
    }
    while(curB->next){
        lenB++;
        curB=curB->next;
    }
    
    //尾结点不相等则不相交
    if(curA!=curB){
        return NULL;
    }
    //相交:找相交位置
    //长度的差距
    int gap=abs(lenA-lenB);  
    
    //另一种思路:先假设长短,再通过if语句修正
    struct ListNode* longList=headA;
    struct ListNode* shortList=headB;
    if(lenA<lenB){
        longList=headB;
        shortList=headA;
        //此时就longList一定代表长链表,shortList一定代表短链表,就不用区分AB了
    }
    //移动差距步gap
    for(int i=0;i<gap;i++){
        longList=longList->next;
    }
    //再逐个比较是否相等,相等的位置即是相交位置起点
    while(longList!=shortList){
        longList=longList->next;
        shortList=shortList->next;
    }
    return longList;
}

9.环形链表

image-20221107121355837

image-20221107120944816

image-20221107120922714

思路:快慢指针

假设快指针移动速度是慢指针的两倍,如果不带环,那么最终慢指针永远追不上快指针,而如果带环,那么快指针慢指针都进入环后,相当于是一个追及问题了,最后总有一时,使得快慢指针在同一位置,说明带环

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
bool hasCycle(struct ListNode *head) {
    struct ListNode* slow=head;
    struct ListNode* fast=head;
    //奇数个偶数个结点
    while(fast&&fast->next){           //while的停止条件是针对不带环的情况
        slow=slow->next;
        fast=fast->next->next;
        
        if(slow==fast){
            return true;
        }
    }
    return false;      
}

【扩展问题】

1.为什么快指针每次走两步,慢指针走一步可以追上?

假设链表带环,两个指针最后都会进入环,快指针先进环,慢指针后进环。当慢指针刚进环时,可能就和快指针相遇了,最差情况下两个指针之间的距离刚好就是环的长度。 此时,两个指针每移动一次,之间的距离就缩小一步(fast比slow多走一步),不会出现每次刚好是套圈的情况,因此:在慢指针走到一圈之前,快指针肯定是可以追上慢指针的,即相遇(一圈之内即可以追上)

2.为什么slow一次走一步,fast一次走两步,而fast一次走3步,走4步,…n步行吗?

假设slow每次1步,fast每次走3步。当slow进环后,假设fast和slow之间间隔距离是N,每追击一次fast和slow之间的距离缩小2;因此如果N是偶数可以直接追上,若N是奇数则会在最后不会恰好相遇,而是被反超,开启了新的一轮追击;设C代表环的周长,那么fast和slow之间的距离变成了C-1,再次追击根据时,若C-1是偶数,则可以追上,而C-1是奇数,那么追完后仍然会出现与第一相同情况的反超问题,永远追不上!

因此:相对速度差1步一定能追上,而其它情况不一定能追上

10.环形链表 II

image-20221107133828229

思路1:

【结论】:一个指针head从首结点开始,另一个指针meet从相遇位置开始,一次向后移动,meet和head的相遇位置即是入口点

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
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;
            //让meet和head移动,相交位置即是入口点
            while(head!=meet)
            {
                head=head->next;
                meet=meet->next;
            }
            return meet;
        }
    }
    return NULL;
}

【证明结论】:

核心关系:快指针走的路程一定是慢指针的路程的二倍

思路2:

沿用【14.相交链表】的函数,由于要找两个链表的入口点,不妨将环形链表看成一个链表,而未形成环的部分看作另一个链表,将利用这个函数找到这两个链表的相交位置即是入口点

由于存在环形链表,我们可以将环形链表的meet结点的下一结点作为头节点,meet结点作为尾结点,将这个环形链表“断开”

image-20221107170334329

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */

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 *otherhead = meet->next;
            meet->next = NULL;

            return getIntersectionNode(head, otherhead);    //使用判断相交链表的函数
        }
    }
    return NULL;
}
本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到