当前位置: 首页 > news >正文

字符串与链表刷题集(5.30-6.6)

1.字符串的两种写法

char* s = "hello"; char* s1[] = "hello";

前者被编译器放在文字常量区(只读数据段)

s只是一个指针,不能写:

s[0] = "a";

这是在修改只读内存,程序将直接出现段错误崩溃。

数组形式的写法会让s1在栈或静态区拥有一份拷贝,这个副本就是可修改的。

2.找出字符串中第一个只出现一次的字符

int FirstUniqChar(char* str) { int count[256] = { 0 }; int sz = strlen(str); int i = 0; for (i = 0; i < sz; i++)//统计所有与元素出现的次数 { unsigned char ch = str[i]; count[ch]++;//字符的本质是ASCII,所以直接使用下标作为索引 } for (i = 0; i < sz; i++) { unsigned char ch = str[i];//注意索引仍需按照str的顺序来 if (count[ch] == 1) { return i; } } return -1; }

这是用数组模拟哈希表的写法,时间复杂度(n)。

重点在于,我们需要理解,字符本质是ASCII码值,也就是数字,所以它可以作为索引来记录自身出现的次数。

比如,a在某个字符串里出现了2次,那么我们创建遍历字符串后,count[97]这个位置就是2.

3.力扣题:两个链表模拟相加

2. 两数相加 - 力扣(LeetCode)

struct ListNode* addTwoNumbers(struct ListNode* l1, struct ListNode* l2) { //建立哨兵位 struct ListNode dummy; dummy.next = NULL; //建立辅助指针tail,便于后续定位链表尾部 struct ListNode* tail = &dummy; //建立进位记录数carry int carry = 0; while (l1 || l2 || carry)//只要l1和l2有一个未到末尾,则仍需进行相加操作,同时carry若不为零,说明有进位需要则仍需进行相加操作 { int sum = carry;//用sum继承上一位的进位数 //对应位相加 if (l1) { sum += l1->val; l1 = l1->next; } if (l2) { sum += l2->val; l2 = l2->next; } carry = sum / 10;//记录进位数 sum %= 10;//记录本次相加结果 //建立新节点 struct ListNode* node = (struct ListNode*)malloc(sizeof(struct ListNode)); node->val = sum; node->next = NULL; //将tail移动至尾部以此链接新节点 tail->next = node; tail = node; } return dummy.next;//用哨兵位访问链表首部并返回 }

本题真正的难点在于如何在链表中实现整数相加过程中进位的继承,以及对于单次计算结果,我们如何将其和过去的结果进行连接,最后就是使用哨兵位对情况的处理进行统一化。

显然,这需要对取模和除法运算以及链表实现的相关算法有一定理解,其中对变量carry和sum的处理尤为巧妙。

详情看注释。

4.力扣题:反转链表

https://leetcode.cn/problems/reverse-linked-list

题目要求我们反转一个没有前驱的单链表,实际上就是在考验我们头插相关的操作。

我们只需从头遍历链表,把每一个元素头插到新链表即可完成反转操作。

当然这里最好设立一个临时的哨兵节点以同一处理空链表的情况,具体代码如下:

typedef struct ListNode list; struct ListNode* reverseList(struct ListNode* head) { list* pcur1 = head; list dummy; dummy.next = NULL; list* guard = &dummy; while (pcur1) { list* NewNode = pcur1; pcur1 = pcur1->next; NewNode->next = guard->next; guard->next = NewNode; } return guard->next; }

这里,我们先用一个结构体指针NewNode记录当前位置,再让pcur向后移动,这样做的好处是,即使pcur移动了,我们仍然可以访问它未移动时的后方节点。

之后的代码很好理解,让NewNode的后继指向头节点,也就是哨兵节点的下一个,之后再把哨兵节点重新移动到头部,即让它的后继指向NewNode。

5.力扣题:合并两个升序链表

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

解决这个问题,比较容易理解的思路是,依次从两个链表中选择数据较小的节点,插入到新链表中,直到某个链表来到尾部,再将另一个剩下的所有节点按原顺序插入到新链表中即可完成合并。

这里解释一下后半句。

由于两个链表是升序的,按照我们的取法——每次取二者较小的那个节点——只要一个链表来到末尾,那么另一个链表剩下的所有节点必然大于新链表的所有节点,所以直接按原顺序插入即可。

具体代码如下:

list* mergeTwoLists(list* head1, list* head2) { assert(head1 && head2); list dummy; dummy.next = NULL; list* tail = &dummy; list* cur1 = head1; list* cur2 = head2; while (cur1 && cur2) { if (cur1->x <= cur2->x) { tail->next = cur1; cur1 = cur1->next; } else { tail->next = cur2; cur2 = cur2->next; } tail = tail->next; } if (cur1) { tail->next = cur1; } else { tail->next = cur2; } return dummy.next; }

这里我们创建了一个tail来维护新链表的尾部,每当完成一次插入,tail就向后移动一位,直到cur1或者cur2来到尾部。

6.力扣题:寻找交叉节点

160. 相交链表 - 力扣(LeetCode)

这里先考虑简单的思路。我们先依次遍历两个链表计算出二者长度,再相减计算差值k。之后再让两个指针遍历一边,但这次,我们让处于长链表的指针先走k步,这样当两个指针相遇时,必然是到达了交叉地点。

typedef struct ListNode list; struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) { list *cur1 = headA, *cur2 = headB; int lenA = 0, lenB = 0; while (cur1) { cur1 = cur1->next; lenA++; } cur1 = headA; while (cur2) { cur2 = cur2->next; lenB++; } cur2 = headB; int k = lenA - lenB; if (k >= 0) { while (k--) cur1 = cur1->next; } else { k = -k; while (k--) cur2 = cur2->next; } while (cur1 != cur2) { cur1 = cur1->next; cur2 = cur2->next; } return cur1; }

当然,更优雅的解决方案是使用快慢指针。

还是两个指针遍历两个链表,但当某一个指针来到末尾时,我们让它跳到另一个链表的头部继续走,这样由于两个指针走的路程是相等的,那么必然会在交叉点相遇,具体的代码实现如下:

typedef struct ListNode list; struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) { list *cur1 = headA, *cur2 = headB; while (cur1 != cur2) { cur1 = (cur1 == NULL) ? headB : cur1->next; cur2 = (cur2 == NULL) ? headA : cur2->next; } return cur1; }

7.力扣题:重排链表

LCR 026. 重排链表 - 力扣(LeetCode)

我们可以先试用快慢指针找到链表的中间节点,截取链表的后半部分,再使用刚刚实现过的反转方法,把后半段链表反转,再按题目要求的规则插入原链表,具体的代码如下:

typedef struct ListNode list; list* FindMin(list* head) { list* fast = head; list* slow = head; while (fast && fast->next) { slow = slow->next; fast = fast->next->next; } return slow; } list* revertlist(list* head) { list dummy; dummy.next = NULL; list* guard = &dummy; list* pcur = head; while (pcur) { list* next = pcur->next; pcur->next = guard->next; guard->next = pcur; pcur = next; } return guard->next; } void reorderList(struct ListNode* head){ //寻找中间节点 list* mid = FindMin(head); list* newhead = mid->next; mid->next = NULL; //反转后半段链表 list* revert_listhead = revertlist(newhead); //按规则插入 list* pcur = head; list* rpcur = revert_listhead; while (rpcur) { list* next = pcur->next; list* rnext = rpcur->next; rpcur->next = next; pcur->next = rpcur; pcur = next; rpcur = rnext; } }

但是这样的方法显然不够优雅。

我们知道,需要插入的节点和其插入的位置在下标上是有关系的,比如,第n节点要插入到下标0的后方,而第n - 1节点要插入到下标1的后方,如果我们能按下标访问链表,那这个问题就非常简单了。

这里提供一种思路。

我们可以先试用一个结构体类型的数组记录每一个节点指针,让后用索引i,j来访问数组,从而访问节点进行插入操作,具体代码如下:

typedef struct ListNode list; void reorderList(struct ListNode* head){ list* arr[50000]; list* pcur1 = head; int n = 0; for (n = 0; pcur1 != NULL; n++, pcur1 = pcur1->next) { arr[n] = pcur1; } int i = 0, j = 0; for (j = n - 1, i = 0; j > n / 2; j--, i++) { arr[j]->next = arr[i]->next; arr[i]->next = arr[j]; } arr[j]->next = NULL; }

这里一定需要注意n在走完第一个循环后变成了节点的总个数,所以j一定要从n - 1开始访问,否则就会越界。

再一个就是最后一定要将链表末尾next置空,否则链表就会变成循环。

8.力扣题:无重复的最长子串

3. 无重复字符的最长子串 - 力扣(LeetCode)

在数据不大时,这道题可以使用暴力解法。

int lenthIfUniquel(char* s, int t)//判断传入的区间是否有重复元素 { int len = t + 1; for (int i = 0; i < len; i++) { for (int j = i + 1; j < len; j++) { if (s[i] == s[j]) return 1;//由于最少传入长度为1,故返回1 } } return len; } int lengthOfLongestSubstring(char* s) { char* start = s; int sz = strlen(s); if (sz == 1) return 1; int tmpsz = 1;//start最多向后访问多少个字符 int len = 0; while (*start) { int tmp = 0, tmpsz = 1; while (tmpsz <= sz - 1) { tmp = lenthIfUniquel(start, tmpsz); if (len < tmp)//更新最大长度 len = tmp; tmpsz++;//铆钉左边界后,不断扩展右边界 } start++; sz--; } return len; }

从字符串头部开始,依次选取区间,再判断这个区间是否有重复字符,若有则返回1,若无则返回子串长度。每轮检查后,都不断更新最大长度,最终得到最大非重复子串。

但显然,这个暴力解法的时间复杂度来到了惊人的N^3,肯定通过不了所有测试用例。

本题最高效的解法是使用哈希表,但为了照顾还未学习哈希表的同学,我们用更加简单易懂的方式来解决。

在第二道题中,我们已经知道一种思路:字符本质上是数字,因此可以使用数组来记录它出现的次数,我们可以把这个思路应用到这里:

以索引 i 作为左边界,j 作为右边界,i作为第一层循环,从字符串头部遍历,j 作为内层循环从 i开始遍历。创建一个数组seen,把遍历过的字符作为下标将1存入数组,表示这个字符在区间i - j已经存在。

如果访问到数组seen里某一个下标下的元素是1,就意味着重复,就跳出内层循环,让i++,即右移左边界,继续检查,如果未检查到重复,就利用j 和 i不断更新当前字符串长度。

代码如下:

int lengthOfLongestSubstring(char* s) { int i = 0, j = 0; int sz = strlen(s); int max_len = 0; for (i = 0; i < sz; i++) { int seen[5000] = { 0 }; for (j = i; j < sz; j++) { if (seen[(unsigned char)s[j]]) break; seen[(unsigned char)s[j]] = 1; max_len = MAX(max_len, j - i + 1); } } return max_len; }
http://www.jsqmd.com/news/973465/

相关文章:

  • 科研信息流操作系统:arXiv自动化+结构化笔记+知识图谱闭环
  • 新能源车企的整车故障排查标准(15):故障诊断综合案例与思维训练
  • 2026年镇江CPPM课程班期费用怎么核对?众智商学院官网400冯老师资料咨询 - 众智商学院职业教育
  • 第32章:AI辅助去中心化身份(DID)——链上可验证凭证
  • 豆包 LeetCode 3082. 求出所有子序列的能量和 Java实现
  • 3分钟掌握百度网盘直链解析:告别限速的完整指南
  • 手把手教你排查华为桌面云FusionAccess用户登录失败问题(附详细日志分析)
  • 终极游戏语言障碍终结者:XUnity.AutoTranslator完整指南
  • 【Redis分布式缓存实战】第18章 Redis全方位性能调优
  • 第33章:AI辅助SocialFi开发——Lens协议集成
  • IDEA + Maven Assembly Plugin:一条命令打包含所有依赖的JavaFX Jar,再用exe4j生成轻量exe
  • 广元母婴除甲醛CMA甲醛检测治理公司深度测评:绿呼吸环保稳居榜首 - 一修哥咨询
  • 赣州母婴除甲醛CMA甲醛检测治理公司深度测评:绿呼吸环保稳居榜首 - 一修哥咨询
  • PHP代码迁移与版本升级指南
  • 可形变模型原理与实战:从PCA降维到足部三维参数化建模
  • 手把手教你用RT-Thread点亮CH32V307开发板的LED,并搞定串口打印(附完整工程)
  • B站光科教程之外:Light Tools新手快速上手的5个隐藏技巧和界面冷知识
  • 别再只测平面了!手把手教你用Apriltag和Homography矩阵实现3D姿态解算
  • PID无线调参进阶:基于HC-05蓝牙和SerialPlot,打造你的移动调试工作站
  • 拒绝暴力洗稿!2026年实测横评10款免费降AI工具:搞定去AIGC痕迹与学术表达双标准 - 降AI实验室
  • 富阳母婴除甲醛CMA甲醛检测治理公司深度测评:绿呼吸环保稳居榜首 - 一修哥咨询
  • AI生成excel表格“AI导出鸭”:结构化数据流转的深度测评与工程实证
  • 2026年众智商学院PMP班期确认加微信怎么问?官网400冯老师考前冲刺咨询 - 众智商学院职业教育
  • RAGFlow 使用指南:从部署到构建 AI 知识库
  • 第35章:AI辅助开发者工具——自动生成ABI文档与TypeScript类型
  • Android启动安全实战:手把手教你用avbtool给dtbo.img镜像签名(附完整命令)
  • 2026电脑显示器选购:高端方案解析与避坑指南 - 服务品牌热点
  • 阜新母婴除甲醛CMA甲醛检测治理公司深度测评:绿呼吸环保稳居榜首 - 一修哥咨询
  • 深度解锁NVIDIA显卡潜能:Profile Inspector完全使用手册
  • 多 SIM 协作 (DSDS/DSDA) 架构文档