文章目录
剑指offer题库总结(三)之链表(C语言版本)
题24. 反转链表
-
题目描述:反转链表
给定一个单链表的头结点pHead(该头节点是有值的,比如在下图,它的val是1),长度为n,反转该链表后,返回新链表的表头。
数据范围: 0≤n≤1000
要求:空间复杂度 O(1)O(1) ,时间复杂度 O(n)O(n) 。
如当输入链表{1,2,3}时,经反转后,原链表变为{3,2,1},所以对应的输出为{3,2,1}。 -
解题思路:采用递归的方法。
假如链表只有2个元素,那么为:head head-next这两个节点,分别是节点0和节点1。反转就需要将节点1指向节点0:head->next->next = head;节点0指向null:head->next = NULL。每次都调用这两步操作。递归的终止条件:head或者head->next为空,说明链表遍历完成。
/**
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*
* C语言声明定义全局变量请加上static,防止重复定义
*/
/**
* @param pHead ListNode类
* @return ListNode类
*/
struct ListNode* ReverseList(struct ListNode* pHead ) {
// write code here
if(pHead == NULL || pHead->next == NULL) //终止条件
{
return pHead;
}
struct ListNode *Reverse = ReverseList(pHead->next); //
pHead->next->next = pHead;
pHead->next = NULL;
return Reverse;
}
题6.从尾到头打印链表
-
题目描述:
输入一个链表的头节点,按链表从尾到头的顺序返回每个节点的值(用数组返回)。
如输入{1,2,3}的链表如下图:
返回一个数组为[3,2,1]
0 <= 链表长度 <= 10000 -
示例:
输入:{1,2,3}
返回值:[3,2,1] -
解题思路:
1.考虑边界值的判断,如果不合法返回值为-1;
- 明确题目要求返回值是一个数组,那需要获取该链表的长度作为数组的大小,在堆中开辟一块数组空间,同时倒序填写到数组中。
/**
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*
* C语言声明定义全局变量请加上static,防止重复定义
*/
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param listNode ListNode类
* @return int整型一维数组
* @return int* returnSize 返回数组行数
*/
int* printListFromTailToHead(struct ListNode* listNode, int* returnSize ) {
// write code here
if(!listNode)
{
return -1;
}
struct ListNode *p = listNode;
int len = 0;
//判断链表有多少个元素
while(p)
{
p = p->next;
len++;
}
//在堆中开辟一块数组空间。
int *Arr = (int *)malloc(len * sizeof(int));
p = listNode;
for(int i = len - 1; i >= 0; i--)
{
Arr[i] = p->val;
p = p->next;
}
*returnSize = len;
return Arr;
}
题18. 删除链表的节点
- 题目描述:
给定单向链表的头指针和一个要删除的节点的值(前提:链表中各值都不相等),定义一个函数删除该节点。返回删除后的链表的头节点。 - 数据范围:
0<=链表节点值<=10000
0<=链表长度<=10000 - 示例:
输入:{2,5,1,9},5
返回值:{2,1,9}
说明:给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 2 -> 1 -> 9 - 解题思路:
1.边界条件:如果非法,直接返回head;
2.因为链表无法随机访问元素,必须从链表头部开始遍历,先需要查找到该元素的位置,采用双指针,一个指针为另一个指针的下一个元素,如:p = p2->next;
3.删除元素:
(1)情况1:元素就在头结点,那直接返回head->next;
(2)情况2:元素不在头结点,那采用双指针遍历并删除,将前一个结点直接指向当前结点的下一个指针就删除了当前结点:p2->next = p->next;(实际p->next就是p2->next->next)。
/**
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*
* C语言声明定义全局变量请加上static,防止重复定义
*/
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param head ListNode类
* @param val int整型
* @return ListNode类
*/
struct ListNode* deleteNode(struct ListNode* head, int val ) {
// write code here
if(head == NULL)
{
return head;
}
struct ListNode *p = head;
struct ListNode *p2 = head;
int temp;
if(p->val == val)
{
return head->next;
}
while(p->next)
{
temp = p->val;
if(temp == val)
{
p2->next = p->next;
return head;
}
p2 = p;
p = p->next;
}
return head;
}
题22. 链表中倒数最后k个结点
-
题目描述:
输入一个长度为 n 的链表,设链表中的元素的值为 ai ,返回该链表中倒数第k个节点。
如果该链表长度小于k,请返回一个长度为 0 的链表。 -
数据范围:0≤n≤105,0≤ai≤109,0 ≤k≤10^9
-
要求:空间复杂度 O(n),时间复杂度 O(n)
-
进阶:空间复杂度 O(1),时间复杂度 O(n)
-
例如:输入{1,2,3,4,5},2时,对应的链表结构如下图所示:
其中蓝色部分为该链表的最后2个结点,所以返回倒数第2个结点(也即结点值为4的结点)即可,系统会打印后面所有的节点来比较。 -
示例1:
输入:{1,2,3,4,5},2
返回值:{4,5}
说明:返回倒数第2个节点4,系统会打印后面所有的节点来比较。 -
示例2:
输入:{2},8
返回值:{} -
解题思路:
方法一:先通过遍历的方式获取链表长度len,然后判断len与k的大小,若k大则返回空链表,否则从头遍历到len-k的位置。
/**
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*
* C语言声明定义全局变量请加上static,防止重复定义
*
* C语言声明定义全局变量请加上static,防止重复定义
*/
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param pHead ListNode类
* @param k int整型
* @return ListNode类
*/
struct ListNode* FindKthToTail(struct ListNode* pHead, int k ) {
// write code here
if((!pHead) || (0 >= k))
{
return NULL;
}
struct ListNode *tmp = (struct ListNode *)malloc(sizeof(struct ListNode));
int len = 0;
tmp = pHead;
while(tmp)
{
len++;
tmp = tmp->next;
}
if(k > len)
{
return NULL;
}
else
{
tmp = pHead;
for(int i = 0; i < len - k; i++)
{
tmp = tmp->next;
}
return tmp;
}
}
- 方法二:
双指针移动的方法,一个指针先移动k步,然后第2个指针和第1个指针同时移动,那么第2个指针就与第一个指针相差k步,当第一个指针到达链表尾部,那么返回第2个指针(此时它指的位置就是倒数第k个节点)。
/**
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*
* C语言声明定义全局变量请加上static,防止重复定义
*
* C语言声明定义全局变量请加上static,防止重复定义
*/
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param pHead ListNode类
* @param k int整型
* @return ListNode类
*/
struct ListNode* FindKthToTail(struct ListNode* pHead, int k ) {
// write code here
if((!pHead) || (0 >= k))
{
return NULL;
}
struct ListNode *tmp1 = (struct ListNode *)malloc(sizeof(struct ListNode));
struct ListNode *tmp2 = (struct ListNode *)malloc(sizeof(struct ListNode));
tmp1 = pHead;
tmp2 = pHead;
int i;
for(i = 0; i < k; i++)
{
if(tmp1 != NULL)
{
tmp1 = tmp1->next;
}
else //k大于链表长度时,返回空链表
{
return NULL;
}
}
while(tmp1)
{
tmp1 = tmp1->next;
tmp2 = tmp2->next;
}
return tmp2;
}
题52. 两个链表的第一个公共结点
- 题目描述:
输入两个无环的单向链表,找出它们的第一个公共结点,如果没有公共节点则返回空。(注意因为传入数据是链表,所以错误测试数据的提示是用其他方式显示的,保证传入数据是正确的) - 数据范围: n≤1000
- 要求:空间复杂度 O(1),时间复杂度 O(n)
例如,输入{1,2,3},{4,5},{6,7}时,两个无环的单向链表的结构如下图所示:
可以看到它们的第一个公共结点的结点值为6,所以返回结点值为6的结点。
- 返回值描述:
返回传入的pHead1和pHead2的第一个公共结点,后台会打印以该节点为头节点的链表。 - 示例1
输入:{1,2,3},{4,5},{6,7}
返回值:{6,7} - 说明:第一个参数{1,2,3}代表是第一个链表非公共部分,第二个参数{4,5}代表是第二个链表非公共部分,最后的{6,7}表示的是2个链表的公共部分 这3个参数最后在后台会组装成为2个两个无环的单链表,且是有公共节点的 。
- 解题思路:
- 方法1:常规思路
(1)先判断两个链表是否为NULL,只要其中一个为NULL,则没法相交。
(2)先获取每个链表的长度,分别记为len1和len2,然后比较做差值diff = len1-len2;如果diff>=0,则让pHead1先走diff步,否则让pHead2先走diff步,然后两个指针同时走,当两个指针pHead1 == pHead2时,说明两个指针同时指向一个节点了。返回其中一个指针即可。
/**
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*
* C语言声明定义全局变量请加上static,防止重复定义
*
* C语言声明定义全局变量请加上static,防止重复定义
*/
/**
*
* @param pHead1 ListNode类
* @param pHead2 ListNode类
* @return ListNode类
*/
struct ListNode* FindFirstCommonNode(struct ListNode* pHead1, struct ListNode* pHead2 ) {
// write code here
if(pHead1==NULL||pHead2==NULL)
{
return NULL;
}
struct ListNode *p1 = pHead1;
struct ListNode *p2 = pHead2;
int len1 = 0;
int len2 = 0;
int diff = 0; //记录两个链表相差步数。
//先获取链表的长度
while(p1)
{
len1++;
p1 = p1->next;
}
while(p2)
{
len2++;
p2 = p2->next;
}
diff = len1 - len2;
//哪个链表长,则先移动diff步
if(diff >=0)
{
//前面计算了链表长度,已经指向空了,指针复位到链表头结点。
p1 = pHead1;
p2 = pHead2;
for(int i =0; i < diff; i++)
{
p1 = p1->next;
}
for(int j = 0; j < len2; j++)
{
if(p1 == p2)
{
return p1;
}
else
{
p1 = p1->next;
p2 = p2->next;
}
}
}
else //diff<0
{
p1 = pHead1;
p2 = pHead2;
for(int i = 0; i < 0 - diff; i++)
{
p2 = p2->next;
}
for(int j = 0; j < len1; j++)
{
if(p1 == p2)
{
return p1;
}
else
{
p1 = p1->next;
p2 = p2->next;
}
}
}
return NULL;
}
- 方法2:两个指针遍历2个链表
(1)先判断两个链表是否为NULL,只要其中一个为NULL,则没法相交。
(2)两个链表同时开始走,分别遍历pHead1和pHead2,由于不知道两个链表的长度,则当其中一个链表先到达NULL时,则将该指针指向另一个链表的头指针继续一起遍历。这样到达公共结点时,两个指针的移动步数相等,只是过程不一样。pHead1走了a+c+b;pHead2走了b+c+a步(a+c是pHead1的长度,b+c是pHead2的长度)。
/**
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*
* C语言声明定义全局变量请加上static,防止重复定义
*
* C语言声明定义全局变量请加上static,防止重复定义
*/
/**
*
* @param pHead1 ListNode类
* @param pHead2 ListNode类
* @return ListNode类
*/
struct ListNode* FindFirstCommonNode(struct ListNode* pHead1, struct ListNode* pHead2 ) {
// write code here
if(pHead1==NULL||pHead2==NULL)
{
return NULL;
}
struct ListNode *p1 = pHead1;
struct ListNode *p2 = pHead2;
int i=0;
int j=0;
while(1)
{
if(p1 == p2)
{
return p1;
}
p1 = p1->next;
p2 = p2->next;
if(p1 == NULL)
{
p1 = pHead2;
i++;
if(i == 2) //只能出现一次从p1的尾部重新指向到p2的头部,否则就是无交集
{
return NULL;
}
}
if(p2 == NULL)
{
p2 = pHead1;
j++;
if(j == 2)
{
return NULL;
}
}
}
}
题25. 合并两个排序的链表
-
题目描述:
输入两个递增的链表,单个链表的长度为n,合并这两个链表并使新链表中的节点仍然是递增排序的。 -
数据范围: 0≤n≤1000,−1000≤节点值≤1000
-
要求:空间复杂度 O(1),时间复杂度 O(n)
如输入{1,3,5},{2,4,6}时,合并后的链表为{1,2,3,4,5,6},所以对应的输出为{1,2,3,4,5,6},转换过程如下图所示:
或输入{-1,2,4},{1,3,4}时,合并后的链表为{-1,1,2,3,4,4},所以对应的输出为{-1,1,2,3,4,4},转换过程如下图所示:
-
示例1
输入:{1,3,5},{2,4,6}
返回值:{1,2,3,4,5,6} -
示例2
输入:{},{}
返回值:{} -
示例3
输入:{-1,2,4},{1,3,4}
返回值:{-1,1,2,3,4,4} -
解题思路:
初始化:定义cur指向新链表的头结点,有一个技巧:创建单链表,都会设一个虚拟头结点,也叫哨兵,因为这样每一个结点都有一个前驱结点。 -
操作:
如果p1指向的结点值小于等于p2指向的结点值,则将p1指向的结点值链接到cur的next指针,然后p1指向下一个结点值
否则,让p2指向下一个结点值
循环步骤1,2,直到p1或者p2为nullptr
将p1或者p2剩下的部分链接到cur的后面。
/**
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*
* C语言声明定义全局变量请加上static,防止重复定义
*/
/**
*
* @param pHead1 ListNode类
* @param pHead2 ListNode类
* @return ListNode类
*/
struct ListNode* Merge(struct ListNode* pHead1, struct ListNode* pHead2 ) {
// write code here
int tmp;
struct ListNode *pNew = ( struct ListNode *)malloc(sizeof( struct ListNode));
struct ListNode *pNewTemp = pNew;
if((pHead1 == NULL) && (pHead2 == NULL))
{
return NULL;
}
while((pHead1) && (pHead2))
{
//比较两个指针的大小
if(pHead1->val <= pHead2->val)
{
pNewTemp->next = pHead1;
pHead1 = pHead1->next;
}
else
{
pNewTemp->next = pHead2;
pHead2 = pHead2->next;
}
pNewTemp = pNewTemp->next;
}
//当其中一个链表已经移动到尾部后,则将pNewTemp的next指针指向非空指针,相当于将剩下的成员拼接。
pNewTemp->next = pHead1 ? pHead1 : pHead2;
return pNew->next; //虚拟指针向下next一次。
}
题76.删除链表中重复的结点
题目描述:
在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表 1->2->3->3->4->4->5 处理后为 1->2->5
-
数据范围:链表长度满足 0≤n≤1000 ,链表中的值满足 1≤val≤1000
-
进阶:空间复杂度 O(n),时间复杂度 O(n)
例如输入{1,2,3,3,4,4,5}时,对应的输出为{1,2,5},对应的输入输出链表如下图所示:
-
示例1
输入:{1,2,3,3,4,4,5}
返回值:{1,2,5} -
示例2
输入:{1,1,1,8}
返回值:{8} -
解题思路:
采用双指针,一个表示当前节点的指针cur,另一个指针表示当前节点的下一个节点diff,然后进行比较。主要伪代码如下:
if((pHead = NULL) || (pHead->next == NULL))
说明就是空链表或者只有一个节点的链表,直接return pHead;
然后遍历链表:
如果当前节点cur->val == diff->val && diff不是NULL,说明cur和diff是重复节点:
diff = diff->next; diff指向下一个节点也cur继续比较,知道不相等且不为NULL
if(cur->next == diff && diff) //表示cur和diff是值不相等的
pre->next = cur; //创建的pre指针是cur的前一个结点。
pre = cur;
cur = diff;
else //值相等
cur = diff;
/**
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*
* C语言声明定义全局变量请加上static,防止重复定义
*/
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param pHead ListNode类
* @return ListNode类
*/
struct ListNode* deleteDuplication(struct ListNode* pHead ) {
// write code here
if((pHead == NULL) || (pHead->next == NULL))
{
return pHead;
}
//创建一个新节点,可以规避掉头节点是重复值的情况,达到统一处理的效果
struct ListNode *preHead = (struct ListNode *)malloc(sizeof(struct ListNode));
preHead->next = pHead;
struct ListNode *pre = preHead; //指向当前节点的前一个结点
struct ListNode *cur = pHead; //指向当前节点
while(cur)
{
//创建一个节点表示当前节点的下一个节点,用来和当前节点对比
struct ListNode *diff = cur->next;
while((diff) && (diff->val == cur->val)) //当前结点和下一个节点的值重复了
{
diff = diff->next;
}
if(cur->next == diff) //如果代表前后两个数不同,那么当前节点下一个节点就是diff,可以更新pre和cur
{
pre->next = cur;
pre = cur;
cur = diff;
}
else
{
cur = diff; //这里表示当前cur有重复值,那么就把cur更新到下一个数,pre不动
}
}
pre->next = cur; //实际这里跳出循环,即cur此时为NULL
return preHead->next;
}
题23.链表中环的入口结点
- 题目描述
给一个长度为n链表,若其中包含环,请找出该链表的环的入口结点,否则,返回null。 - 数据范围: n≤10000,1<=结点值<=10000
- 要求:空间复杂度 O(1),时间复杂度 O(n)
例如,输入{1,2},{3,4,5}时,对应的环形链表如下图所示:
可以看到环的入口结点的结点值为3,所以返回结点值为3的结点。 - 示例1
输入:{1,2},{3,4,5}
返回值:3
说明:返回环形链表入口结点,我们后台程序会打印该环形链表入口结点对应的结点值,即3
输入:{1},{}
返回值:“null”
说明:没有环,返回对应编程语言的空结点,后台程序会打印"null"
输入:{},{2}
返回值:2
说明:环的部分只有一个结点,所以返回该环形链表入口结点,后台程序打印该结点对应的结点值,即2
- 解题思路:
方法1:类似hash法,记录每次遍历到的结点
因为每个节点的值都是大于0的,小于10000,定义一个指针遍历,遍历到当前的就取负,如果遍历到一个节点他的值是负的就证明以前遍历过了,也就有环了,取个相反数返回就行。
空间复杂度 O(1),时间复杂度 O(n),符合题意。
/**
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*
* C语言声明定义全局变量请加上static,防止重复定义
*
* C语言声明定义全局变量请加上static,防止重复定义
*/
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param pHead ListNode类
* @return ListNode类
*/
struct ListNode* EntryNodeOfLoop(struct ListNode* pHead ) {
// write code here
if(pHead == NULL)
{
return pHead;
}
struct ListNode *p = pHead;
while(p != NULL && p->next != NULL)
{
if(p->val > 0)
{
p->val = 0 - p->val; //值取相反数,表示其曾经遍历到了
p = p->next;
}
else //该值小于0则是曾经遍历到了,因为题中说所有元素都是大于0的。
{
p->val = 0 - p->val; //把值再做相反数,还原回来。
return p;
}
}
return NULL;
}
方法2:快慢指针
通过定义slow和fast指针,slow每走一步,fast走两步,若是有环,则一定会在环的某个结点处相遇(slow == fast),根据下图分析计算,可知从相遇处到入口结点的距离与头结点与入口结点的距离相同。
空间复杂度 O(1),时间复杂度 O(n),符合题意。
利用快慢指针遍历链表,快指针每次移动2步,慢指针每次移动1步,若链表带环,则两指针一定会在环中相遇。那么接下来要找这个环的入口了。
我们假设从头结点到环形入口结点的结点数为x。环形入口结点到fast指针与slow指针相遇结点,结点数为y。从相遇结点再到环形入口结点,结点数为z。如图所示:
当两个指针相遇时,slow指针走过的节点数为x + y,fast指针走过的节点数为x + y + n (y + z)(n为fast指针在环内走了n圈才遇到slow指针,(y+z)为 一圈内节点的个数A)。
因为fast指针是一步走两个结点,slow指针一步走一个结点, 所以 fast指针走过的结点数 = slow指针走过的结点数 * 2 即(x + y) * 2 = x + y + n (y + z) 故可得: x + y = n (y + z)。
那么环形的入口即x的距离:x = n (y + z) - y => x = (n - 1) (y + z) + z
当 n为1的时候, x = z,即从头结点出发一个指针,从相遇结点也出发一个指针,这两个指针每次只走一个结点, 那么当这两个指针相遇的时候就是环形入口的结点。
所以在相遇结点处定义一个指针index1,在头结点定义一个指针index2。它们同时移动,每次移动一个结点,当两者相遇时就是环形入口的结点。n大于1同理,index1指针在环里多转了(n-1)圈后,然后再遇到index2,相遇点依然是环形的入口节点。
/**
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*
* C语言声明定义全局变量请加上static,防止重复定义
*
* C语言声明定义全局变量请加上static,防止重复定义
*/
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param pHead ListNode类
* @return ListNode类
*/
struct ListNode* EntryNodeOfLoop(struct ListNode* pHead ) {
// write code here
if(pHead == NULL)
{
return pHead;
}
//定义2个快慢指针
struct ListNode *fast = pHead;
struct ListNode *slow = pHead;
while(fast != NULL && fast->next != NULL)
{
fast = fast->next->next; //快指针每次移动2步
slow = slow->next; //慢指针每次移动1步
if (slow == fast) {
struct ListNode *index1 = fast; //相遇结点
struct ListNode *index2 = pHead; //重新定义一个指针指向头结点
//相遇结点和头指针同时移动,当两个结点相等则是环结点的入口
while (index1 != index2) {
index1 = index1->next;
index2 = index2->next;
}
return index2; // 返回环的入口
}
}
return NULL;
}
单题更新请关注: 知乎账号