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

简介: 顺序表 链表刷题【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
目录
相关文章
|
9天前
|
存储
顺序表和链表(2)
【10月更文挑战第23天】
顺序表和链表(2)
|
10天前
|
存储 C语言
【数据结构】手把手教你单链表(c语言)(附源码)
本文介绍了单链表的基本概念、结构定义及其实现方法。单链表是一种内存地址不连续但逻辑顺序连续的数据结构,每个节点包含数据域和指针域。文章详细讲解了单链表的常见操作,如头插、尾插、头删、尾删、查找、指定位置插入和删除等,并提供了完整的C语言代码示例。通过学习单链表,可以更好地理解数据结构的底层逻辑,提高编程能力。
37 4
|
12天前
|
存储 C语言
【数据结构】顺序表(c语言实现)(附源码)
本文介绍了线性表和顺序表的基本概念及其实现。线性表是一种有限序列,常见的线性表有顺序表、链表、栈、队列等。顺序表是一种基于连续内存地址存储数据的数据结构,其底层逻辑是数组。文章详细讲解了静态顺序表和动态顺序表的区别,并重点介绍了动态顺序表的实现,包括初始化、销毁、打印、增删查改等操作。最后,文章总结了顺序表的时间复杂度和局限性,并预告了后续关于链表的内容。
38 3
|
10天前
|
存储 算法 数据管理
顺序表和链表(1)
【10月更文挑战第22天】
|
1月前
|
存储 缓存 C语言
C语言:链表和数组有什么区别
C语言中,链表和数组是两种常用的数据结构。数组是一种线性结构,元素在内存中连续存储,通过下标访问,适合随机访问且大小固定的情况。链表由一系列不连续的节点组成,每个节点存储数据和指向下一个节点的指针,适用于频繁插入和删除操作的场景,链表的大小可以动态变化。
|
1月前
|
存储 安全 Java
【用Java学习数据结构系列】探索顺序表和链表的无尽秘密(附带练习唔)pro
【用Java学习数据结构系列】探索顺序表和链表的无尽秘密(附带练习唔)pro
22 3
|
10天前
|
C语言
【数据结构】双向带头循环链表(c语言)(附源码)
本文介绍了双向带头循环链表的概念和实现。双向带头循环链表具有三个关键点:双向、带头和循环。与单链表相比,它的头插、尾插、头删、尾删等操作的时间复杂度均为O(1),提高了运行效率。文章详细讲解了链表的结构定义、方法声明和实现,包括创建新节点、初始化、打印、判断是否为空、插入和删除节点等操作。最后提供了完整的代码示例。
28 0
|
30天前
|
机器学习/深度学习 编译器 C语言
C语言刷题(中)(保姆式详解)
C语言刷题(中)(保姆式详解)
12 0
|
30天前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
32 3
|
23小时前
|
C语言
c语言调用的函数的声明
被调用的函数的声明: 一个函数调用另一个函数需具备的条件: 首先被调用的函数必须是已经存在的函数,即头文件中存在或已经定义过; 如果使用库函数,一般应该在本文件开头用#include命令将调用有关库函数时在所需要用到的信息“包含”到本文件中。.h文件是头文件所用的后缀。 如果使用用户自己定义的函数,而且该函数与使用它的函数在同一个文件中,一般还应该在主调函数中对被调用的函数做声明。 如果被调用的函数定义出现在主调函数之前可以不必声明。 如果已在所有函数定义之前,在函数的外部已做了函数声明,则在各个主调函数中不必多所调用的函数在做声明
16 6