顺序表 链表刷题【C语言实现】

1. 顺序表

说明:由于nums[src]等价于*(nums + src),故以下将将下标计数器简化称作指针

1.1 移除元素

在这里插入图片描述

思路:

在这里插入图片描述

题目要求的空间复杂度O(1)的言外之意是不另外开辟数组空间,直接在原数组上进行删除元素的操作。一般情况下:如果只删除数组中的其中一个元素,只需将它后面的所有元素向前移动一位,将它覆盖即删除。而这道题有多个要删除的元素,核心思想也是一样的

“将它后面的所有元素向前移动一位”的前提条件是后面所有元素都不等于val(要删除的数)

在这里插入图片描述

那么有多个val的情况也是类似地,以val为分界线
在这里插入图片描述

如何达到分割数组的效果?

dst和src指针同时出发,当遇到val,dst停下,让src继续走直到不遇到val为止,然后再同时走,直到src(src走的快)遍历完数组。这相当于将等于val的元素跳过,分割数组的目的就是删除等于val的元素。注意:dst和src在同时走时,要将src的值赋给dst

  • 循环终止条件:src遍历完数组
  • 返回值:dst
int removeElement(int* nums, int numsSize, int val){
    int dst = 0;
    int src = 0;
    while(src < numsSize)
    {
        if(nums[src] == val)
        {
            src++;
        }
        else
        {
            nums[dst] = nums[src];
            dst++;
            src++;
        }
        
    }
    return dst;
}

1.2 删除有序数组中的重复项

在这里插入图片描述

思路:

在这里插入图片描述

int removeDuplicates(int* nums, int numsSize){
    int dst = 0;
    int src = 0;
    while(src < numsSize)
    {
        if(nums[dst] == nums[src])
        {
            src++;
        }
        else
        {
            nums[++dst] = nums[src++];//注意这里是将src位置的值赋值给dst的下一个位置
        }
    }

    return dst + 1;
}

找到每个分界点,且将它们组成的区间内的元素删除,思路和上一题类似:dst记录覆盖后的位置,src找下一个分界点。当src找到分界点,将其值赋值给dst的下一个位置(因为dst当前的位置是相对于src的上一个分界点),然后src继续找,直到遍历完数组。

  • 循环终止条件:src遍历完数组
  • 返回修改后的数组长度:dst+1(dst是下标,从0开始)

1.3 合并两个有序数组

在这里插入图片描述

思路:
在这里插入图片描述

这是归并排序的核心思想,前提是要比较的两个数组已经排好顺序

  • 循环终止条件:至少有一个数组遍历完毕。实际上,这里num1一定会遍历完的,因为num1剩余的空间要存放num2
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n){
    //都是下标
		int end = nums1Size - 1;
    int end1 = m - 1;
    int end2 = n - 1;
    while(end1 >= 0 && end2 >= 0)//把小的放进去
    {
        if(nums1[end1] > nums2[end2])
        {
            nums1[end] = nums1[end1];
            end--;
            end1--;
        }
        else
        {
            nums1[end] = nums2[end2];
            end--;
            end2--;
        }
    }
    while(end2 >= 0)//num2可能有剩余元素
    {
        nums1[end] = nums2[end2];
        end--;
        end2--;
    }
    return nums1;
}

1.4 轮转数组

在这里插入图片描述

思路:
在这里插入图片描述

这种轮转(左旋/右旋)的题,不论是轮转字符串、数组,都可以用这种思想:各部分逆置+整体逆置。

  • 重点是用k找到分界线
  • 逆置功能写成单独的函数
  • 当k大于数组长度,将k模上长度即可
//逆置函数
void reverse(int* nums, int start, int end)
{
    int left = start;
    int right = end - 1;
    while(left < right)
    {
        int tmp = nums[left];
        nums[left] = nums[right];
        nums[right] = tmp;
        left++;
        right--;
    }
}

void rotate(int* nums, int numsSize, int k){
    k %= numsSize;
    if(k < numsSize)
    {
        reverse(nums, 0, numsSize - k);//前一部分
        reverse(nums, numsSize - k, numsSize);//后一部分
        reverse(nums, 0, numsSize);//整体
    }
}

1.5 数组形式的整数加法

在这里插入图片描述

2. 链表

2.1 移除链表元素

在这里插入图片描述

思路:

在这里插入图片描述

因为单向链表的局限性,只能用三个指针维护要删除节点的前中后三个节点

  • 三指针同时迭代的条件:cur->val != val
  • 让prev指向next之前,要先用cur得到next。并且要先free掉cur当前的值再更新cur的位置
  • 删除的节点若为第一个节点,则需要更新head的地址

struct ListNode* removeElements(struct ListNode* head, int val){
    struct ListNode* cur = head;
    struct ListNode* prev = NULL;
    while(cur)
    {
        if(cur->val == val)
        {
            struct ListNode* next = cur->next;
            //如果要删除的是第一个节点
            if(prev == NULL)//cur指向head,prve为空
            {
                free(cur);
                head = next;//更新第一个节点
                cur = head;//迭代cur
            }
            else
            {
                prev->next = next;//迭代prev
                free(cur);
                cur = next;//迭代cur
            }
        }
        else
        {
            prev = cur;
            cur = cur->next;//迭代
        }
    }
    return head;
}

注:其实next指针不用每次都更新它,因为只有在碰到要删除的节点要需要用到它,所以这里将最后一个else语句中的next迭代放到了第二个if语句中

2.2 反转链表

在这里插入图片描述

思路:遍历,反向

在这里插入图片描述

此思路比较简单,同样是三指针同时迭代,为了可读性,这里将要返回的指针命名为newhead

struct ListNode* reverseList(struct ListNode* head){
    struct ListNode* cur = head;
    struct ListNode* newhead = NULL;
    while(cur)
    {
        struct ListNode* next = cur->next;//保存cur的下一个节点
        cur->next = newhead;//cur逆指
        newhead = cur//更新
        cur = next;//更新
    }
    return newhead;
}

2.3 链表的中间结点

在这里插入图片描述

思路:

在这里插入图片描述

快慢指针是一个经典方法,十分巧妙。接下来会介绍快慢指针的更多用法。

  • 奇数个节点循环终止的条件是迭代到最后一个节点,偶数个节点循环终止的条件是迭代到NULL(即最后一个节点的next)。所以循环终止的条件有两个。
struct ListNode* middleNode(struct ListNode* head){
    struct ListNode* fast = head;
    struct ListNode* slow = head;
    while(fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
    }
    return slow;
}

为什么循环条件是&&而不是||

2.4 链表中倒数第k个结点

在这里插入图片描述

思路:

在这里插入图片描述

让快指针先走k步,作用是锁定了两指针之间的距离

  • 注意判断链表是否为空和k是否大于链表长度
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k ) {
    int len = 0;
    struct ListNode* fast = pListHead;
    struct ListNode* slow = pListHead;
    struct ListNode* cur = pListHead;
    while(cur)//计算链表的长度
    {
        len++;
        cur = cur->next;
    }
    if(len == 0 || k > len)
    {
        return NULL;
    }
    
    while(k--)//让fast先走k步
    {
        fast = fast->next;
    }
    
    while(fast)//让快慢指针同时走
    {
        fast = fast->next;
        slow = slow->next;
    }
    return slow;
}

2.5 合并两个有序链表

在这里插入图片描述

思路:

在这里插入图片描述

这是归并排序的核心思想

  • 新链表的每次更新,都要更新cur
  • 注意链表为空的情况。一共有三种情况:只有list1为空;只有list2为空;全为空。如何处理?
  • 若原链表长度不同,最后一定会有剩下的节点,只需将它们链接到新链表的当前位置即可。同样需要判断是哪一个链表还剩。
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){
    struct ListNode* l1 = list1;
    struct ListNode* l2 = list2;
    struct ListNode* listhead = NULL;
    //判断链表为空
    if(l1 == NULL)
    {
        return l2;
    }
    if(l2 == NULL)
    {
        return l1;
    }
    //把第一个val小的节点作为listhead
    struct ListNode* cur = NULL;//迭代指针变量
    if(l1->val < l2->val)
    {
        listhead = cur = l1;
        l1 = l1->next;
    }
    else
    {
        listhead = cur = l2;
        l2 = l2->next;
    }
    while(l1 && l2)
    {
        if(l1->val < l2->val)
        {
            cur->next = l1;
            l1 = l1->next;
        }
        else
        {
            cur->next = l2;
            l2 = l2->next;
        }
        cur = cur->next;
    }
    //此时至少有一个走完,将剩下的连接上
    if(l1 == NULL)
    {
        cur->next = l2;
    }
    else
    {
        cur->next = l1;
    }
    return listhead;
}

2.6 链表分割

注:头节点(哨兵位节点)和第一个节点不同,头节点仅存放了第一个节点的地址,不存放数据,第一个节点才是真正有数据的节点。可以理解为头节点就是一个代号,代表着这个链表

在这里插入图片描述

思路:

在这里插入图片描述

其核心思路为:先分组,再链接。

  • 链接时注意不要链接到要free掉的节点
  • free掉开辟的内存
ListNode* partition(ListNode* pHead, int x) {
        struct ListNode* cur = pHead;
        struct ListNode* greaterHead, *greaterTail, *lesstail, *lesshead;
				//为两个头节点开辟内存
        greaterHead  = greaterTail = (struct ListNode*)malloc(sizeof(struct ListNode));
        lesstail  = lesshead = (struct ListNode*)malloc(sizeof(struct ListNode));
        lesstail->next = NULL;
        greaterTail->next = NULL;
		    //分组
        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;//跳过头节点再链接
        greaterTail->next = NULL;
        pHead = lesshead->next;//更新pHead
        free(lesshead);
        free(greaterHead);
        return pHead;
    }

2.7 链表的回文结构

在这里插入图片描述

思路:

在这里插入图片描述

为什么会出现最后那个巧合?

因为将后半部分逆置时并没有处理中间节点前面的节点(这里是2),也就是说,倒置完以后,2依然是指向3的。这个巧妙的巧合就免去了当链表节点个数为奇数时,还要增加一系列条件判断,例如节点为个数奇数时,cur2后半部分的最后一个节点不需要比较等(因为对称轴没法跟谁比)。

class PalindromeList {
public:
    bool chkPalindrome(ListNode* A) {
        ListNode* fast = A;
        ListNode* slow = A;
        ListNode* cur1 = A;
        ListNode* cur2 = NULL;
        while(fast->next && fast)//找中
        {
            fast = fast->next->next;
            slow = slow->next;
        }
        //此时slow指向中间节点
        while(slow)//逆置后一半
        {
            ListNode* next = slow->next;
            slow->next = cur2;
            cur2 = slow;
            slow = next;
        }
				//此时cur2是后半部分的第一个节点
        while(cur1 && cur2)
        {
            if(cur1->val != cur2->val)
            {
                return false;
            }
            else//迭代
            {
                cur1 = cur1->next;
                cur2 = cur2->next;
            }
        }
					//当前面
        return true;
    }
};

2.8 相交链表

在这里插入图片描述

思路:

在这里插入图片描述

struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    struct ListNode * cur1 = headA;
    struct ListNode * cur2 = headB;
    struct ListNode * cur11 = headA;
    struct ListNode * cur22 = headB;
    if(headA == NULL || headB == NULL)
    {
        return NULL;
    }
    int len1 = 0, len2 = 0;
    //计算链表的长度
    while(cur1)
    {
        len1++;
        cur1 = cur1->next;
    }
    while(cur2)
    {
        len2++;
        cur2 = cur2->next;
    }
    int k = abs(len1 - len2);//得出链表长度之差的绝对值
    if(len1 > len2)
    {
        while(k--)
        {
            cur11 = cur11->next;
        }
    }
    else
    {
        while(k--)
        {
            cur22 = cur22->next;
        }
    }
    //此时两链表处于同一起始点(距离交点的长度)
    while(cur11 && cur22)
    {
        if(cur11 != cur22)
        {
            cur11 = cur11->next;
            cur22 = cur22->next;
        }
        else
        {
            return cur22;
        }
    }
    return NULL;
}

更新日志

4/15/2022
man9o
posted @ 2022-12-06 22:31  shawyxy  阅读(23)  评论(0编辑  收藏  举报