数据结构——顺序表&&单链表oj详解

之前我们讲解了顺序表和单链表这两个数据结构,但是仅仅掌握它们的实现方法是远远不够的,我们还需要通过刷算法题对已有知识进行巩固和利用。

27. 移除元素 - 力扣(LeetCode)

思路一:这一题我们可以想到一种十分简单的思路:再创建一个同等大小数组arr,从0开始遍历原来的数组nums,如果当前被遍历到的元素的值不等于val,那就将这个值放到arr数组中,遍历完以后,arr数组中的元素个数就是原来数组中值不为val的元素个数,再将这些元素放到nums数组中

代码

复制代码
int removeElement(int* nums, int numsSize, int val)
{
    if (numsSize == 0)
    {
        return 0;
    }
    //思路一:创建一个新的数组arr,将nums数组中值不为val的元素放到arr中
    int arr[numsSize];
    int k = 0;
    for (int i = 0; i < numsSize; i++)
    {
        if (nums[i] != val)
        {
            arr[k++] = nums[i];
        }
    }
    for (int i = 0; i < k; i++)
    {
        nums[i] = arr[i];
    }
    return k;
}

这个算法的时间复杂度是O(N),空间复杂度也是O(N)。

有没有什么方法使得空间复杂度降为O(1)呢?

有的兄弟们有的。

思路二:那就是双指针法:我们定义两个"指针"a和b,初始时两个指针都指向数组的起始位置,然后让其中一个指针b往前探路,如果b指向的元素不为val,就让a所指向的位置的值等于b所指向的位置的值,然后再让a和b同时往前走,如果b所指向的元素等于val,就让b往前走,a不动。最终a之前的元素个数就是数组中值不为val的元素个数

代码

复制代码
int removeElement(int* nums, int numsSize, int val)
{
    //思路二,双指针法
    int a = 0, b = 0;
    while (b < numsSize)
    {
        if (nums[b] != val)
        {
            nums[a] = nums[b];
            a++;
        }
        b++;
    }
    return a;
}

26. 删除有序数组中的重复项 - 力扣(LeetCode)

思路一:创建一个新的数组arr,将arr的第一个元素设为nums[0],不断遍历原数组,如果遍历到的元素与arr数组中最后一个元素不同,就将这个元素的值放到arr数组中当前最后一个元素的下一个位置。

代码

复制代码
int removeDuplicates(int* nums, int numsSize)
{
    //创建新的数组
    if (numsSize == 0)
    {
        return 0;
    }
    int arr[numsSize];
    int a = 1, b = 0;
    arr[0] = nums[0];
    while (a < numsSize)
    {
        if (nums[a] != arr[b])
        {
            arr[++b] = nums[a];
        }
        a++;
    }
    for (int i = 0; i <= b; i++)
    {
        nums[i] = arr[i];
    }
    return b + 1;
}

上面的代码定义了两个变量,分别指向了两个数组。

上面代码的时间复杂度为O(N),空间复杂度O(N)。

其实,这个思路也可以用另一种写法,不用创建一个新的数组,而是直接在原数组的基础上进行改动。这种方法跟上一题的第二种思路相似,也是双指针法:

复制代码
//双指针法
int removeDuplicates(int* nums, int numsSize)
{
    int b = 0, a = 1;
    while (a < numsSize)
    {
        if (nums[a] != nums[b] && b != a)
        {
            nums[++b] = nums[a];
        }
        a++;
    }
    return b + 1;
}

88. 合并两个有序数组 - 力扣(LeetCode)

思路:我们知道的是nums1这个数组的空间大于nums2这个数组的空间,并且nums1中多余的空间(没有存放有效元素的空间)在nums1的后面,所以在考虑合并两个数组的元素时,我们可以考虑先从nums1中后面的元素填充值,我们已知两个数组原来是按照非递减序列排列的,并且要求合并后的数组也是按照非递减序列排列,这样的话,我们就要考虑优先将元素值较大的元素往后放,也就是要从两个数组的最后一个有效数据开始比较元素大小,从后往前存放元素,基于此,我们可以写出代码:

复制代码
//从后往前,由大到小开始插入
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n)
{
    int a = m - 1, b = n - 1, c = m + n - 1;
    while (a >= 0 && b >= 0)
    {
        if (nums1[a] >= nums2[b])
        {
            nums1[c] = nums1[a--];
        }
        else
        {
            nums1[c] = nums2[b--];
        }
        c--;
    }
    while (b >= 0)
    {
        nums1[c--] = nums2[b--];
    }
}

203. 移除链表元素 - 力扣(LeetCode)

思路:我们可以创建一个新的链表,注意哦,此处创建新的链表并不是重新去创建每个节点,然后将遍历原链表然后获取新链表中每个节点的值。我们是要根据原有的节点创建新链表,新链表中的节点就是原来的链表的节点,遍历原链表,将值不为val的节点尾插至新链表。

代码:

复制代码
typedef struct ListNode ltnode;
struct ListNode* removeElements(struct ListNode* head, int val)
{
    //如果原链表为空,直接返回
    if (head == NULL)
    {
        return head;
    }
    ltnode* newhead = NULL;
    ltnode* newtail = NULL;
    ltnode* pcur = head;
    while (pcur)
    {
        if (pcur->val != val)
        {
            //尾插
            if (newhead == NULL)
            {
                newhead = newtail = pcur;
            }
            else
            {
                newtail->next = pcur;
                newtail = newtail->next;
            }
        }
        pcur = pcur->next;
    }
    //需要先检查新链表是否为空再让newtail的下一个节点为空
    if (newhead)
        newtail->next = NULL;
    return newhead;
}

其实上面的代码还可以在进行优化,我们可以像双链表那样为新链表设置一个哨兵位节点(即带头节点),这样就可以直接将节点尾插到新链表中了:

复制代码
typedef struct ListNode ltnode;
struct ListNode* removeElements(struct ListNode* head, int val)
{
    ltnode* newhead = (ltnode*)malloc(sizeof(ltnode));
    newhead->next = NULL;
    ltnode* newtail = newhead;
    ltnode* pcur = head;
    while (pcur)
    {
        if (pcur->val != val)
        {
            newtail->next = pcur;
            newtail = newtail->next;
        }
        pcur = pcur->next;
    }
    newtail->next = NULL;
    return newhead->next;

}

可以看到,优化后的代码相较于上一个代码简洁了许多。

206. 反转链表 - 力扣(LeetCode)

这道题的思路就比较新奇了,我们先直接来看代码,结合图解来理解这道题的做法:

复制代码
 typedef struct ListNode ltnode;
struct ListNode* reverseList(struct ListNode* head) 
{
  if(head==NULL)
  {
    return NULL;
  }
    ltnode*p1=NULL,*p2=head,*p3=head->next;
    while(p2)
    {
      p2->next=p1;
      p1=p2;
      p2=p3;
      if(p3)
      p3=p3->next;
    }
    return p1;
}

876. 链表的中间结点 - 力扣(LeetCode)

思路1:计数法,遍历整个链表,记录链表的长度,然后再循环找到中间位置

复制代码
typedef struct ListNode ltnode;
struct ListNode* middleNode(struct ListNode* head)
{
    //思路一:计数法,先遍历链表统计链表的总长度,然后再据此遍历链表找到链表的中间节点
    int len = 0;
    ltnode* pcur = head;
    while (pcur)
    {
        len++;
        pcur = pcur->next;
    }
    len = len / 2;
    pcur = head;
    while (len--)
    {
        pcur = pcur->next;
    }
    return pcur;
}

思路2:快慢指针:慢指针每次走一步,快指针每次走两步,当快指针走到尽头时,慢指针恰好走到中间位置,这就好像初中学到的物理中的位移与时间的问题,当slow和fast的时间相同,他们的位移量就决定于自己的速度,所以当fast走到尽头的时候,slow的位移量刚好是fast的一半:

复制代码
 typedef struct ListNode ltnode;
struct ListNode* middleNode(struct ListNode* head) 
{
    ltnode*slow=head,*fast=head;
    while(fast&&fast->next)//注意顺序:不能倒过来哦,如果倒过来,就会发生空指针解引用的问题
//而由于短路原理,此处当fast为空的时候,就会直接跳出循环,不会导致空指针解引用的问题
    {
      slow=slow->next;
      fast=fast->next->next;
    }
    return slow;
}

21. 合并两个有序链表 - 力扣(LeetCode)

这道题也可以创建一个带有空头结点的新链表,然后遍历原来的两个链表,比较后将节点插入到新的链表:

复制代码
typedef struct ListNode ltnode;
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) 
{
  if(list1==NULL)
  {
    return list2;
  }
  if(list2==NULL)
  {
    return list1;
  }
    ltnode*head=(ltnode*)malloc(sizeof(ltnode));
    ltnode*tail=head;
    head->next=NULL;
    while(list1&&list2)
    {
      if(list1->val<=list2->val)
      {
        tail->next=list1;
        tail=tail->next;
        list1=list1->next;
      }
      else
      {
        tail->next=list2;
        tail=tail->next;
        list2=list2->next;
      }
    }
    if(list1)
    {
      tail->next=list1;
    }
    if(list2)
    {
      tail->next=list2;
    }
    ltnode*newnode=head->next;
    free(head);
return newnode;
}

联系到这里,才真的算开始游戏。这一道题是比较有挑战性的一道题,但是有了我们前面那些题目的铺垫,这道题的解题步骤就比较简单了,主要这个思路应该该怎么想捏?

比如题目给的样例:1->2->2->1,他是一个回文数组,因为从中间开始往后如果我们将链表翻转过来,那么反转后的部分就变成了:1->2,此时翻转后的部分与从开头到中间部分的内容对应相等。

这就为我们提供了思路:我们可以先找到中间位置,然后从中间位置向后开始反转链表,如果反转之后的链表与从开头位置到中间的节点值对应相等,那就说明这是一个回文结构的链表,而我们之前又凑巧写了寻找中间节点和翻转链表的代码,正好可以CV(复制粘贴)到这里,简直是天助我也。思路有了,就来上手写代码吧:

复制代码
typedef struct ListNode ltnode;
//寻找中间节点:
    struct ListNode* middleNode(struct ListNode* head) {
        ltnode* slow = head, *fast = head;
        while (fast &&
                fast->next) //注意顺序:不能倒过来哦,如果倒过来,就会发生空指针解引用的问题
//而由于短路原理,此处当fast为空的时候,就会直接跳出循环,不会导致空指针解引用的问题
        {
            slow = slow->next;
            fast = fast->next->next;
        }
        return slow;
    }
//从中间节点开始向后反转链表:

    struct ListNode* reverseList(struct ListNode* head) {
        if (head == NULL) {
            return NULL;
        }
        ltnode* p1 = NULL, *p2 = head, *p3 = head->next;
        while (p2) {
            p2->next = p1;
            p1 = p2;
            p2 = p3;
            if (p3)
                p3 = p3->next;
        }
        return p1;
    }
    bool chkPalindrome(ListNode* A) {
        // write code here
        //思路:先找到链表的中间节点,从中间开始向后反转链表,看反转后的链表是否与原来的链表相同
        ltnode* midnode = middleNode(A);
        ltnode*revs=reverseList(midnode);
        ltnode*pcur1=A,*pcur2=revs;
        while(pcur2)
        {
            if(pcur2->val!=pcur1->val)
            {
                return false;
            }
            pcur2=pcur2->next;
            pcur1=pcur1->next;
        }
        return true;
    }

除了这种思路,还有没有啥比较简单的思路呢?

有的兄弟们,有的。

我们注意到,题目中特地提醒我们了:链表的长度不大于900,这就让我们有了可乘之机。我们可以将链表转化为数组,然后这个题目就转换为了判断数组是否是回文结构,这就简单多了呀,既如此,快来上手写代码:

复制代码
typedef struct ListNode ltnode;
    bool chkPalindrome(ListNode* A) {
        // write code here
        int arr[900];
        int size=0;//表示数组中有效数据个数
        ltnode*pcur=A;
        while(pcur)
        {
            arr[size++]=pcur->val;
            pcur=pcur->next;
        }
        //将链表转换为数组之后,就开始判断数组是否是回文结构
        int left=0,right=size-1;
        while(left<=right)
        {
            if(arr[left]!=arr[right])
            {
                return false;
            }
            right--;
            left++;
        }
        return true;
    }

这个思路可就简单太多了,但是这是因为题目让我们抓住了漏洞,如果题目没有告诉我们链表的最大长度,那就目前只能使用方法一了,这是最保险的方法。

138. 随机链表的复制 - 力扣(LeetCode)

假设没有random指针,就是普通的来链表,要让我们复制一份,我们就只需要创建节点,为每个节点的val对应赋值,指定next指针的指向即可。而这道题最大的难点就是找到拷贝后每个节点的random指针应该指向哪里。

可以想一下:我们能否利用原链表去找到我们要复制的目标链表中每个节点的random指针的指向?

我们可以在原来链表中的每个节点之间插入一个新的节点,这个节点的值与它前一个节点的值相同,如图:

这样一来,我们就不难发现,我们插入节点的random指针应该指向的节点与它前一个节点的random节点的next指针指向的节点。也就是说原链表中节点A的random指针的next指针的指向就是A的next指针指向的节点(插入节点)的random指针的指向。

所以,这道题的思路就是:先插入新节点,为新节点的random指针指定指向,将新节点从原链表中割除并连成新链表。

分析了这么多,终于能开始写代码了:

复制代码
typedef struct Node node;
 //创建新节点
 node* buynode(int val)
 {
    node*newnode=(node*)malloc(sizeof(node));
    newnode->val=val;
    return newnode;
 }
struct Node* copyRandomList(struct Node* head) 
{
    if(head==NULL)
    {
      return head;
    }
	    //将新节点插入到原链表
      node* pcur=head;
      node*newnode;
      while(pcur)
      {
        newnode=buynode(pcur->val);
        newnode->next=pcur->next;
        pcur->next=newnode;
        pcur=newnode->next;
      }
      //为新节点的random指针指定指向:新节点random指针的指向是他的前一个节点random指针的next指针指向的节点
      pcur=head;
      newnode=head->next;
      while(pcur)
      {
        if(pcur->random)
        {
          newnode->random=pcur->random->next;
        }
        else
        {
          newnode->random=NULL;
        }
        pcur=newnode->next;
        if(pcur)
         newnode=pcur->next;
      }
      //将新的链表的节点连接起来:
        node* newhead=head->next;
        node* newtail=newhead;
        pcur=newtail->next;
        while(pcur)
        {
           newtail->next=pcur->next;
           newtail=newtail->next;
           pcur=newtail->next;
        }
     return newhead;
}

今天的oj练习题就讲到这里,数据结构部分小伙伴们一定要好好刷题呀!!!

相关推荐
快去睡觉~11 分钟前
力扣400:第N位数字
数据结构·算法·leetcode
qqxhb1 小时前
零基础数据结构与算法——第七章:算法实践与工程应用-搜索引擎
算法·搜索引擎·tf-idf·倒排索引·pagerank·算法库
GUET_一路向前2 小时前
【C语言】解释形参void *data用法
c语言·开发语言·通用指针
gzzeason2 小时前
LeetCode Hot100:递归穿透值传递问题
算法·leetcode·职场和发展
汤永红2 小时前
week1-[循环嵌套]画正方形
数据结构·c++·算法
yi.Ist3 小时前
图论——Djikstra最短路
数据结构·学习·算法·图论·好难
数据爬坡ing3 小时前
过程设计工具深度解析-软件工程之详细设计(补充篇)
大数据·数据结构·算法·apache·软件工程·软件构建·设计语言
茜子.Java4 小时前
二分算法(模板)
算法
呼啦啦啦啦啦啦啦啦5 小时前
【Java】HashMap的详细介绍
java·数据结构·哈希表