当前位置:   article > 正文

顺序表、链表相关OJ题(1)

顺序表、链表相关OJ题(1)

   

                     

创作不易,友友们给个三连呗!!

     本文为经典算法OJ题练习,大部分题型都有多种思路,每种思路的解法博主都试过了(去网站那里验证)是正确的,大家可以参考!!

一、移除元素(力扣)

经典算法OJ题:移除元素

思路1:遍历数组,找到一个元素等于val,就把后面的所有元素往前挪,类似顺序表实现中的指定位置删除!

  1. //思路1:遍历数组,找到一个元素等于val,就把后面的所有元素往前挪,类似顺序表实现中的指定位置删除!
  2. int removeElement(int* nums, int numsSize, int val)
  3. {
  4. for (int i = 0; i < numsSize; i++)//用来遍历
  5. {
  6. if (nums[i] == val)//要挪动,而且是从前往后挪
  7. {
  8. for (int j = i; j < numsSize - 1; j++)
  9. nums[j] = nums[j + 1];//从前往后挪
  10. numsSize--;//挪完长度-1
  11. i--;//挪动后新的数据还在原来的位置,所以不能让i往前走!!
  12. }
  13. }
  14. return numsSize;
  15. }

思路2:(双指针法)利用双指针,第一个指针引路,第二个指针存放想要的元素(不等于val的元素)(较优)

  1. //思路2:(双指针法)利用双指针,第一个指针引路,第二个指针存放想要的元素(不等于val的元素)
  2. int removeElement(int* nums, int numsSize, int val)
  3. {
  4. int src = 0;//用来探路,src即原操作数
  5. int dst = 0;//用来存放想要的数据,dst即目标操作数
  6. while (src < numsSize)
  7. {
  8. if (nums[src] == val)
  9. {
  10. src++;//找到val就src走
  11. }
  12. else
  13. {
  14. nums[dst] = nums[src];//dst接收想要的数据
  15. //找不到就两个都走
  16. dst++;
  17. src++;
  18. }
  19. }
  20. //此时dst恰好就是数组的新长度
  21. return dst;
  22. }

二、合并两个有序数组(力扣)

经典算法OJ题:合并两个有序数组

思路1:num2全部存储到num1中,再统一进行排序(qsort)

  1. int int_cmp(const void* p1, const void* p2)//比较方法
  2. {
  3. return (*(int*)p1 - *(int*)p2);//返回值来影响qsort
  4. }
  5. void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n)
  6. {
  7. int i = m;//指向数组1后面的空位置
  8. int j = 0;//指向数组2
  9. while (i < m + n)
  10. {
  11. nums1[i] = nums2[j];
  12. i++;
  13. j++;
  14. }
  15. //循环结束说明插入完成,使用快速排序
  16. qsort(nums1, m + n, sizeof(int), int_cmp);
  17. }

思路2:合并的时候顺便排序,利用3个指针,l1用来遍历数组1,l2用来遍历数组2,比大小之后的数据用l3记录。(较优)

  1. void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n)
  2. {
  3. int l1=m-1;//从1数组的最后一个有效数据往前
  4. int l2=n-1;//从2数组的最后一个有效数据往前
  5. int l3=n+m-1;//从1数组的最后一个元素开始往前
  6. while(l1>=0 && l2>=0)//l1和l2其中一个遍历完就得跳出循环
  7. {
  8. //从后往前比大小
  9. if(nums1[l1]>nums2[l2])
  10. nums1[l3--]=nums1[l1--];
  11. else
  12. nums1[l3--]=nums2[l2--];
  13. }
  14. //循环结束后,有两种情况,一种是l1先遍历完,此时l2要接着插进去,
  15. //另一种是l2先遍历完,此时l1就不需要处理了
  16. while(l2>=0)
  17. nums1[l3--]=nums2[l2--];
  18. }

三、移除链表元素(力扣)

经典算法OJ题:移除链表元素

思路1:遍历原链表,遇到val就删除,类似单链表的指定位置删除

  1. typedef struct ListNode ListNode;
  2. struct ListNode* removeElements(struct ListNode* head, int val)
  3. {
  4. //考虑头节点就是val的情况
  5. while(head!=NULL&&head->val==val)
  6. head=head->next;
  7. //此时头节点不可能是val
  8. //当链表为空
  9. if(head==NULL)
  10. return head;
  11. //当链表不为空时
  12. ListNode*pcur=head;//用来遍历链表
  13. ListNode*prev=NULL;//用来记录前驱结点
  14. while(pcur)
  15. {
  16. //当找到val时
  17. if(pcur->val==val)
  18. {
  19. prev->next=prev->next->next;//前驱结点指向pucr的下一个结点
  20. free(pcur);//删除的结点被释放
  21. pcur=prev->next;//继续指向新的结点
  22. }
  23. //没找到val时
  24. else
  25. {
  26. prev=pcur;//往后走之前记录前驱结点
  27. pcur=pcur->next;//pcur往前遍历
  28. }
  29. }
  30. return head;
  31. }

思路2:定义一个不带头新链表,将不为val的结点尾插进去

  1. typedef struct ListNode ListNode;
  2. struct ListNode* removeElements(struct ListNode* head, int val)
  3. {
  4. ListNode*pcur=head;//用来遍历链表
  5. //定义新链表的头尾指针
  6. ListNode*newhead=NULL;//用来记录头
  7. ListNode*newtail=NULL;//用来尾插新链表
  8. while(pcur)
  9. {
  10. if(pcur->val!=val)//不满足val则插入到新链表
  11. {
  12. //一开始链表是空的
  13. if(newhead==NULL)
  14. newhead=newtail=pcur;
  15. //链表不为空了,开始尾插
  16. else
  17. {
  18. newtail->next=pcur;//尾插
  19. newtail=newtail->next;//尾插后向后挪动
  20. }
  21. }
  22. pcur=pcur->next;//pcur要遍历往后走
  23. }
  24. //插入完后要加NULL!还要避免newtail是空的情况
  25. if(newtail)
  26. newtail->next=NULL;
  27. return newhead;
  28. }

思路3:给原链表创造一个哨兵结点,然后遍历,遇到val就删(和思路1比较,多了一个哨兵,稍优于思路1)

  1. typedef struct ListNode ListNode;
  2. struct ListNode* removeElements(struct ListNode* head, int val)
  3. {
  4. ListNode*newhead=(ListNode*)malloc(sizeof(ListNode));//创建一个新的哨兵节点
  5. newhead->next=head;//哨兵接头
  6. ListNode*pcur=head;//用来遍历链表
  7. ListNode*prev=newhead;//记录前驱结点
  8. while(pcur)
  9. {
  10. //遇到了,开始删除
  11. if(pcur->val==val)
  12. {
  13. prev->next=pcur->next;
  14. free(pcur);
  15. pcur=prev->next;
  16. }
  17. //如果没遇到val,都往后走
  18. else
  19. {
  20. prev=pcur;
  21. pcur=pcur->next;
  22. }
  23. }
  24. //循环结束,删除完成
  25. ListNode*ret=newhead->next;//释放哨兵结点前记住需要返回的结点
  26. free(newhead);
  27. newhead=NULL;
  28. return ret;
  29. }

思路4:定义一个带头新链表,将不为val的结点尾插进去(和思路2相比较,多了一个哨兵)(较优)

  1. typedef struct ListNode ListNode;
  2. struct ListNode* removeElements(struct ListNode* head, int val)
  3. {
  4. ListNode*newhead,*newtail;
  5. newhead=newtail=(ListNode*)malloc(sizeof(ListNode));//创建一个新的哨兵节点
  6. ListNode*pcur=head;//用来遍历链表
  7. while(pcur)
  8. {
  9. if(pcur->val!=val)
  10. {
  11. //找打不为val的值 开始尾插
  12. newtail->next=pcur;
  13. newtail=newtail->next;
  14. }
  15. pcur=pcur->next;//没找到就往后找
  16. }
  17. newtail->next=NULL;
  18. ListNode*ret=newhead->next;//释放哨兵时记住返回值
  19. free(newhead);
  20. newhead=NULL;
  21. return ret;
  22. }

四、反转链表(力扣)

经典算法OJ题:反转链表

思路1:利用带头单链表头插法,建立一个新的带头结点的单链表L,扫描head链表的所有结点,每扫描一个结点就创造一个s结点并将值赋给s结点然后头插法插入新链表L中,得到的就是逆序的head链表

  1. typedef struct ListNode ListNode;
  2. ListNode*BuyNode(int x)//封装创建新结点的函数
  3. {
  4. ListNode*newnode=(ListNode*)malloc(sizeof(ListNode));
  5. newnode->next=NULL;
  6. newnode->val=x;
  7. return newnode;
  8. }
  9. struct ListNode* reverseList(struct ListNode* head)
  10. {
  11. ListNode*pcur=head;//用来遍历
  12. ListNode*newhead=BuyNode(-1);//创建哨兵结点
  13. ListNode*temp=NULL;//充当临时变量
  14. while(pcur)
  15. {
  16. temp=BuyNode(pcur->val);//创建新结点接收pur的值
  17. //头插
  18. temp->next=newhead->next;
  19. newhead->next=temp;
  20. //pcur往后走
  21. pcur=pcur->next;//pcur往后走
  22. }
  23. ListNode*ret=newhead->next;//哨兵位释放之前保存头节点
  24. free(newhead);
  25. newhead=NULL;
  26. return ret;
  27. }

思路2:利用带头单链表头插法,建立一个新的带头结点的单链表L,扫描head链表的所有结点,每扫描一个结点就头插法插入新链表L中,得到的就是逆序的head链表(相比思路1多了个哨兵,稍优于思路1)

  1. typedef struct ListNode ListNode;
  2. struct ListNode* reverseList(struct ListNode* head)
  3. {
  4. //如果链表为空
  5. if(head==NULL)
  6. return head;
  7. //如果链表不为空
  8. ListNode*newhead,*newtail;//一个哨兵,一个记录尾巴方便后面置NULL;
  9. newhead=(ListNode*)malloc(sizeof(ListNode));//创建哨兵结点
  10. newhead->next=head;//哨兵和原来的头节点连接起来
  11. newtail=head;//newtail记住一开始的head,方便后面连接NULL
  12. ListNode*pcur=head->next;//pcur用来遍历(从第二个)
  13. ListNode*temp=NULL;//用来记录下一个遍历点
  14. while(pcur)
  15. {
  16. temp=pcur->next;//连接前,先记住下一个结点的位置
  17. //头插 插在哨兵结点和原来头结点的中间
  18. newhead->next=pcur;
  19. pcur->next=head;
  20. head=pcur;//头插进来的成为哨兵结点后面的新头
  21. pcur=temp;//pcur从原先链表的下一个结点开始继续遍历
  22. }
  23. newtail->next=NULL;//要记得给尾巴结点连接NULL;
  24. free(newhead);
  25. newhead=NULL;
  26. return head;
  27. }

思路3:利用不带头链表头插法,扫描head链表的所有结点,每扫描一个结点就头插法插入新链表L中,得到的就是逆序的head链表(较优)

  1. typedef struct ListNode ListNode;
  2. struct ListNode* reverseList(struct ListNode* head)
  3. {
  4. //如果链表为空
  5. if(head==NULL)
  6. return head;
  7. //如果链表不为空
  8. ListNode*pcur=head->next;//用来遍历
  9. ListNode*ptail=head;//用来记录尾巴,方便后面置NULL;
  10. ListNode*temp;//记录遍历的结点
  11. while(pcur)
  12. {
  13. temp=pcur->next;
  14. //头插到head前面
  15. pcur->next=head;
  16. head=pcur;
  17. pcur=temp;
  18. }
  19. ptail->next=NULL;
  20. return head;
  21. }

思路4:利用3个指针,分别记录前驱结点、当前结点、后继结点,改变原链表的指针指向(最优)

  1. typedef struct ListNode ListNode;
  2. struct ListNode* reverseList(struct ListNode* head)
  3. {
  4. //链表为空的时候
  5. if(head==NULL)
  6. return head;
  7. //链表不为空的时候,创建3个指针,分别指向前驱、当前、后继结点
  8. ListNode*p1,*p2,*p3;
  9. p1=NULL;//前驱
  10. p2=head;//当前
  11. p3=head->next;//后继
  12. while(p2)
  13. {
  14. //改变指向
  15. p2->next=p1;
  16. //向后挪动
  17. p1=p2;
  18. p2=p3;
  19. //考虑p3为NULL的时候
  20. if(p3)
  21. p3=p3->next;
  22. }
  23. return p1;
  24. }

五、合并两个有序链表(力扣)

经典算法OJ题:合并两个有序链表

思路1:创建一个哨兵节点,双指针判断两组数据的大小,因为是把 list2 的节点插入 list1 ,所以只要当 list1 指向的数大于 list2 的数,就把当前 list2 节点插入 list1 的前面。循环判定条件,只要双指针中有一个为空就跳出循环,即有一个指针到了节点末端。若 list1 先结束,表示剩下 list2 的数都比 list1 里的数大,直接把 list2 放到 list1后即可若 list2 先结束,即表示已经合并完成。

  1. typedef struct ListNode ListNode;
  2. struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
  3. {
  4. ListNode*newhead=(ListNode*)malloc(sizeof(ListNode));//创建一个新的哨兵结点
  5. newhead->next=list1;//哨兵点与list1相连接
  6. ListNode*p1=list1;//利用p1遍历链表1
  7. ListNode*p2=list2;//利用p2遍历链表2
  8. ListNode*prev=newhead;//prev记录前驱结点
  9. ListNode*temp=NULL;//充当临时变量,暂时保存list2的指向
  10. while(p1&&p2)//p1和p2有一个为NULL了就必须跳出循环
  11. {
  12. if(p1->val>p2->val)//list2插入list1该元素前面
  13. {
  14. temp=p2->next;//记住p2指针的遍历点
  15. //尾插
  16. prev->next=p2;
  17. p2->next=p1;
  18. //尾插完成往前走
  19. prev=p2;
  20. p2=temp;
  21. }
  22. //找不到时,prev和p1都往后走
  23. else
  24. {
  25. p1=p1->next;
  26. prev=prev->next;
  27. }
  28. }
  29. //跳出循环后有两种可能,一种是p1先为NULL,一种是p2先为NULL
  30. //此时prev恰好走到尾结点
  31. //如果p2为NULL,说明已经结束!如果p1为NULL,此时尾插p2在prev后面
  32. if(p1==NULL)
  33. prev->next=p2;
  34. ListNode*ret=newhead->next;//哨兵位要释放,返回前要记录newhead->next
  35. free(newhead);
  36. newhead=NULL;
  37. return ret;
  38. }

思路2:定义一个带头新链表(方便返回),两个指针分别指向两组数组,逐个比较,较小的尾插到新的链表中,循环判断条件,只要有一个指针为NULL就跳出循环,无论是 list1 结束还是 list2 结束,只需要把剩下的部分接在新链表上即可。(较优)

  1. typedef struct ListNode ListNode;
  2. struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
  3. {
  4. ListNode*newhead,*newptail;
  5. newhead= newptail=(ListNode*)malloc(sizeof(ListNode));//创建一个新的哨兵结点
  6. //newptail是用来尾插的
  7. ListNode*p1=list1;//利用p1遍历链表1
  8. ListNode*p2=list2;//利用p2遍历链表2
  9. while(p1&&p2)//p1和p2有一个为NULL了就必须跳出循环
  10. {
  11. if(p1->val<p2->val)
  12. {
  13. newptail->next=p1;//尾插
  14. newptail=newptail->next;//插入后newptail往后走
  15. p1=p1->next;//插入后p1往后走
  16. }
  17. else
  18. {
  19. newptail->next=p2;//尾插
  20. newptail=newptail->next;//插入后newptail往后走
  21. p2=p2->next;//插入后p2往后走
  22. }
  23. }
  24. //跳出循环后有两种可能,一种是p1先为NULL,一种是p2先为NULL
  25. //在newtail后面插入不为NULL的链表。
  26. newptail->next=(p1==NULL?p2:p1);
  27. ListNode*ret=newhead->next;//哨兵位要释放,返回前要记录newhead->next
  28. free(newhead);
  29. newhead=NULL;
  30. return ret;
  31. }

六、链表的中间结点(力扣)

经典算法OJ题:链表的中间结点

思路1:统计链表中结点的个数,然后除以2找到中间结点

  1. typedef struct ListNode ListNode;
  2. struct ListNode* middleNode(struct ListNode* head)
  3. {
  4. int count=0;//用来记录总共的结点数量
  5. ListNode*pcur=head;//用来遍历
  6. while(pcur)
  7. {
  8. pcur=pcur->next;
  9. count++;
  10. }
  11. //此时计算出count,除以2
  12. count=count/2;//此时count代表中间结点的位置
  13. while(count)
  14. {
  15. head=head->next;
  16. count--;
  17. }
  18. return head;
  19. }

思路2:(快慢指针法),创建两个指针一开始都指向头节点,一个一次走一步,一个一次走两步,当快指针为NULL时,慢指针指向的就是中间的位置(较优)

  1. typedef struct ListNode ListNode;
  2. struct ListNode* middleNode(struct ListNode* head)
  3. {
  4. ListNode*fast,*slow;
  5. fast=slow=head;//都指向头结点
  6. while(fast!=NULL&&fast->next!=NULL)//存在一个就得跳出循环
  7. //而且顺序不能反!!!因为与运算符从前往后运算
  8. {
  9. fast=fast->next->next;//走两步
  10. slow=slow->next;//走一步
  11. }
  12. //循环结束slow正好指向中间结点
  13. return slow;
  14. }

七、分割链表(力扣)

经典算法OJ题:分割链表

思路1:创建一个新链表,遍历原链表,小的头插,大的尾插。

  1. typedef struct ListNode ListNode;
  2. struct ListNode* partition(struct ListNode* head, int x)
  3. {
  4. //链表为空
  5. if(head==NULL)
  6. return head;
  7. //链表不为空
  8. ListNode*pcur,*newtail;
  9. pcur=newtail=head;//pcur用来遍历 newtail用来尾插
  10. while(pcur)
  11. {
  12. ListNode * temp=pcur->next;
  13. if(pcur->val<x)
  14. {
  15. //头插
  16. pcur->next=head;
  17. head=pcur;//pcur成为新的头
  18. }
  19. //尾插
  20. else
  21. {
  22. newtail->next=pcur;
  23. newtail=newtail->next;
  24. }
  25. pcur=temp;//继续遍历
  26. }
  27. newtail->next=NULL;
  28. return head;
  29. }

思路2:创建两个新链表,遍历原链表,大的尾插大链表,小的尾插小链表,最后合并在一起。

  1. typedef struct ListNode ListNode;
  2. struct ListNode* partition(struct ListNode* head, int x)
  3. {
  4. if(head==NULL)
  5. return head;
  6. ListNode*bighead,*bigtail,*smallhead,*smalltail;
  7. bighead=bigtail=(ListNode*)malloc(sizeof(ListNode));//大链表哨兵
  8. smallhead=smalltail=(ListNode*)malloc(sizeof(ListNode));//小链表哨兵
  9. ListNode*pcur=head;//pcur用来遍历
  10. while(pcur)
  11. {
  12. if(pcur->val<x)
  13. //尾插小链表
  14. {
  15. smalltail->next=pcur;
  16. smalltail=smalltail->next;
  17. }
  18. else
  19. //尾插大链表
  20. {
  21. bigtail->next=pcur;
  22. bigtail=bigtail->next;
  23. }
  24. pcur=pcur->next;//继续往下走
  25. }
  26. //遍历完成,连接大小链表
  27. smalltail->next=bighead->next;
  28. bigtail->next=NULL;
  29. ListNode*ret=smallhead->next;//记住返回值
  30. free(bighead);
  31. free(smallhead);
  32. return ret;
  33. }

八、环形链表的约瑟夫问题(牛客)

经典算法OJ题:环形链表的约瑟夫问题

思路:创建一个不带头的单向链表,每逢m就删除

  1. typedef struct ListNode ListNode;
  2. ListNode * BuyNode(int x)//创建结点的函数
  3. {
  4. ListNode *newnode=(ListNode *)malloc(sizeof(ListNode));
  5. newnode->next=newnode;
  6. newnode->val=x;
  7. return newnode;
  8. }
  9. int ysf(int n, int m ) {
  10. // write code here
  11. //创建一个不带头的单向循环链表
  12. ListNode *phead=BuyNode(1);//创建一个头节点
  13. ListNode *ptail=phead;//用来遍历
  14. for(int i=2;i<=n;i++)
  15. {
  16. ptail->next=BuyNode(i);
  17. ptail=ptail->next;
  18. }
  19. //创建完后要首尾相连
  20. ptail->next=phead;
  21. ListNode *pcur=phead;//pcur用来遍历
  22. ListNode *prev=NULL;//用来记录前驱结点
  23. int count=1;//用来数数
  24. while(pcur->next!=pcur)//结束条件是场上只剩下一个人
  25. {
  26. if(count==m)
  27. {
  28. //指定位置删除
  29. prev->next=pcur->next;
  30. free(pcur);
  31. pcur=prev->next;
  32. count=1;//重新数
  33. }
  34. else
  35. {
  36. prev=pcur;
  37. pcur=pcur->next;
  38. count++;
  39. }
  40. }
  41. //此时pcur是场上唯一还在的结点
  42. return pcur->val;
  43. }

九、总结

1、顺序表背景的OJ题较为简单,因为顺序表底层是数组,有连续存放的特点,一方面指针运算偏移比较容易(可以多往指针的方向思考),另一方面就是我可以根据下标去拿到我想要的元素,无论是从前遍历还是从后遍历还是从中间都很方便!所以解题思路容易一些,而单链表只能通过指向,并且非双向的链表想从后面或者中间遍历会比较吃力!

2、顺序表背景的题,如果涉及到指定位置插入或者是指定位置删除,需要大量挪动数据,多层for循环比较麻烦,有时候可以往指针运算去思考!

3、链表背景的题,涉及到有关中间结点的,一般是快慢指针!!

4、关于链表的头插,如果是两个链表根据情况插入到一个新链表的头插,那么创建一个哨兵位结点会比较容易点,因为这样可以避免一开始就得换头结点。如果是在原链表的基础上头插,因为原链表是存在头节点的,这个时候不设哨兵位就会简单点,因为可以直接换头。

5、关于链表的尾插,一般需要设置一个tail指针往后遍历。

6、关于链表的指定位置插入或删除,需要记录前驱结点,这个时候需要除了需要考虑头节点为NULL的情况,还要考虑链表只有一个结点的情况,因为这个时候也没有前驱结点,这个时候如果运用哨兵就不需要考虑只有一个结点的情况,因为哨兵位可以充当头结点的前驱结点。

7、哨兵链表容易记住起始地址

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/90166
推荐阅读
相关标签
  

闽ICP备14008679号