今天主要是对一些链表相关问题的理解和处理:
下面所有问题处理的都是这种链表:
struct ListNode
{
int val;
struct ListNode* next;
};
话不多说,我直接进入正题:
1. 删除链表中等于给定值 val 的所有节点。
给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回新的头节点。
思路1:在原链表中进行删除:如果需要头删,那就把head后移并且把首节点释放;用pre来链接cur所释放的节点的下个节点
代码实现:
struct ListNode* removeElements(struct ListNode* head, int val)
{
struct ListNode* pre = NULL;
struct ListNode* cur = head;
while (cur)
{
if (cur->val == val)
{
//头删
if (cur == head)
{
head = cur->next;//注意头删的情况时头是要后移的
free(cur);
cur = head;
}
else
{
//删除
pre->next = cur->next;//这两步顺序不能错
free(cur);//这两步顺序不能错
cur = pre->next;
}
}
else
{
pre = cur;
cur = cur->next;
}
}
return head;
}
思路2:创建一个新的链表,遍历原链表,把所有不要删除的节点插入新的链表中
要注意这里返回的是个新的head,需要留意野指针的问题!
代码实现:
struct ListNode* removeElements(struct ListNode* head, int val)
{
struct ListNode* cur = head;
struct ListNode* tail = NULL;
head = NULL;
while (cur)
{
if (cur->val != val)
{
if (tail == NULL)
{
head = tail = cur;//首插
}
else
{
tail->next = cur;//尾插
tail = tail->next;
}
cur = cur->next;
}
else
{
struct ListNode* del = cur;
cur = cur->next;
free(del);
}
}
if (tail)
{
tail->next = NULL;//要留意防止tail的next野指针
}
return head;
}
基于这个思路,我们优化一下:来个哨兵位的首节点就不用考虑,tail为NULL的情况了。
代码实现一下:
struct ListNode* removeElements(struct ListNode* head, int val)
{
struct ListNode* cur = head;
struct ListNode* tail = NULL;
//哨兵位的头节点
head = tail = (struct ListNode*)malloc(sizeof(struct ListNode));
tail->next = NULL;
while (cur)
{
if (cur->val != val)
{
tail->next = cur;
tail = tail->next;
cur = cur->next;
}
else
{
struct ListNode* del = cur;
cur = cur->next;
free(del);
}
}
tail->next = NULL;
return head->next;
}
注意:
1.即使有了哨兵位的头节点,我们还是要记得把tail->next置为空以结束链表
2.因为有了哨兵位的头节点的存在,最后return 的应该是哨兵位的头节点所指向的那个节点
2. 反转一个单链表。
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
逆置问题
思路1:把链表节点从最后一个一个头插
代码实现:
struct ListNode* reverseList(struct ListNode* head)
{
struct ListNode* newhead = NULL;
struct ListNode* cur = head;
while (cur)
{
struct ListNode* next = cur->next;//用来记录原链表中的下个节点
//头插
cur->next = newhead;
newhead = cur;
cur = next;
}
return newhead;
}
思路2:把指针的方向颠倒
代码实现:
struct ListNode* reverseList(struct ListNode* head)
{
if(head==NULL)
return NULL; //即传的是空链表时没有逆置的意义
struct ListNode* n1, * n2, * n3;
n1 = NULL;
n2 = head;
n3 = n2->next;
while (n2)
{
//逆置
n2->next = n1;
//迭代
n1 = n2;//n1跟着n2走
n2 = n3;//n2往前走一个
//注意一下n3为空时,应该规避野指针问题
if (n3)
{
n3 = n3->next;
}
}
return n1;
这里需要注意的是:当传的链表为空链表时,没有逆置它的意义。
图画的可能有点丑,但是大致的意思是很清晰的表示出来了:以图中得1、2、3、4这四个步骤作为一次颠倒,直到n1走到最后一个节点的时候(也就是n3的next指向NULL)的时候结束。
3. 给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。
给你单链表的头结点 head ,请你找出并返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。1 2 3 4 5 6 -> 4
思路:快慢指针:我们定义一个慢指针和一个快指针,慢指针一次走一步,快指针每一次从slow指向的位置走两步,当快指针到尽头时,慢指针就在中间
代码实现:
struct ListNode* middleNode(struct ListNode* head)
{
struct ListNode* slow, * fast;
slow = fast = head;
while (fast && fast->next)
{
slw = slow->next;
fast = fast->next->next;
}
return slow;
}
这里我们要注意的是:当节点数为奇数和偶数这两种情况是不同的
当节点数为奇数时,slow指向中间节点时fast->NULL
当节点数为偶数时,slow指向中间节点时fast指向最后一个节点
4. 输入一个链表,输出该链表中倒数第k个结点。
输入一个链表,输出该链表中倒数第k个结点。
思路:给快慢指针:快指针先走k步,之后和慢指针一起走,当快指针到NULL时,慢指针就指向倒数第k个节点。
代码实现:
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k)
{
struct ListNode* slow=pListHead;
struct ListNode* fast = pListHead;
int end = k;
while (end )
{
if (fast)
{
fast = fast->next;
}
else
return NULL;
end--;
}
while (fast)
{
slow = slow->next;
fast = fast->next;
}
return slow;
}
5. 将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
思路:分别用两个指针指向两链表的第一个节点,从头比较,取小的尾插到新链表——归并
代码实现:
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
/*if (list1 == NULL)
return list2;
if (list2 == NULL)
return list1;
struct ListNode* head ,*tail;
head = tail = NULL;*/
//上面这是不带哨兵位的写法,下面是带哨兵位的写法
struct ListNode* head, * tail;
head = tail = malloc(sizeof(struct ListNode));
tail->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;
}
struct ListNode* list = head->next;//由于哨兵位的存在
free(head);
return list;
}
6. 编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前
现有一链表的头指针 ListNode* pHead,给一定值x,编写一段代码将所有小于x的结点排在其余结点之前,且不能改变原来的数据顺序,返回重新排列后的链表的头指针。
思路:创建两个哨兵位的头节点,将比x小得尾插到第一个链表,将比x大的尾插到第二个链表。
这里如果不用哨兵位,会在这两个任意一个链表为空时带来麻烦。
代码实现:
struct ListNode* partition(struct ListNode* pHead, int x)
{
//哨兵位的首节点的创建
struct ListNode* greaterhead, * greatertail, * lesshead, * lesstail;
greaterhead = greatertail = (struct ListNode*)malloc(sizeof(struct ListNode));
lesshead = lesstail = (struct ListNode*)malloc(sizeof(struct ListNode));
greatertail->next = NULL;
lesstail->next = NULL;
//尾插
struct ListNode* cur = pHead;
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;//因为有哨兵位,所以是greaterhead的next
greatertail->next = NULL;//这是为了防止greatertail的next不不指向NULL这个特殊情况
//例如输入 315867 x=7
struct ListNode* head = lesshead->next;//同上面一个道理因为有哨兵位所以是lesshead的next
//送走大头兵
free(greaterhead);
free(lesshead);
return head;
}
7. 链表的回文结构。
对于一个链表,请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法,判断其是否为回文结构。
给定一个链表的头指针A,请返回一个bool值,代表其是否为回文结构。
测试样例:1->2->2->1 返回:true
思路:先找到中间节点,然后逆置后半(包括中间节点),如果前半和后半节点一个一个往后比较都是相同的,那就是回文结构
代码实现:
这里偷个懒,调用一下上面我们已经写过的找中间节点和逆置的函数
class PalindromeList
{
public:
bool chkPalindrome(ListNode* A)
{
struct ListNode* head = A;
struct ListNode* mid = middleNode(A);
struct ListNode* rhead = reverseList(mid);
while (head && rhead)
{
if (head->val == rhead->val)
{
head = head->next;
rhead = rhead->next;
}
else
return false;
}
return true;
}
};
注意:
当前半或后半任意一个链表走到尽头了,就代表它全部和另一个链表相等,原链表就是回文结构,结束程序。
这里牛客网上面没有支持c的选项,但是c++是兼容c的,我们直接在里面写c不管它,它自己会认
这里我们其实是侵入式编程了,把人家的东西改了,最好要给他还原成原来的样子,但是这里题目没要求,所以我们放肆一回吧。
8. 输入两个链表,找出它们的第一个公共结点。
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。
例如 a1->a2->b->c->d->e->NULL 和 a->b->->c->d->e->NULL 相交,在b这个节点相交
思路:所谓相交,就是两个链表自某个节点之后所有节点都相同了
方法1:从第一个链表的第一个节点开始,拿它和另外一个链表的所有节点比较,如果都不相同,那就往后走,这个方法的时间复杂度为O(N的平方),效率低下。
方法2:把两个链表的长度先都求出来,让长的先走长度差距的步数,之后同时走看看有没有相同的节点。
代码实现:
struct ListNode* getIntersectionNode(struct ListNode* headA, struct ListNode* headB)
{
if (headA == NULL || headB == NULL)
return NULL;
struct ListNode* curA = headA, * curB = headB;
int lenA = 1;
int lenB = 1;
while (curA->next)
{
curA = curA->next;
lenA++;
}
while (curB->next)
{
curB = curB->next;
lenB++;
}
if (curA != curB)//如果到最后都不相等,那么说明中间就没有交点
{
return NULL;
}
//求第一个交点
struct ListNode* shortList = headA, longList = headB;
if (lenA > lenB)
{
shortList = headB;
longList = headA;
}//这是个很聪明的做法
int gap = abs(lenA - lenB);
//长的先走差距步数
while (gap--)
{
longList = longList->next;
}
while (shortList != longList)
{
//同时走
longList = longList->next;
shortList = shortList->next;
}
return shortList;
}
注意:上面这串代码里面我们没用if语句来分别处理lenA和lenB谁大的情况,而是直接用判断加一个if修改,这是个很聪明的做法。
9. 给定一个链表,判断链表中是否有环。
给你一个链表的头节点 head ,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。
为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。
如果链表中存在环 ,则返回 true .否则,返回 false 。
思路:带环链表一旦遍历就会死循环,很烦
我们可以用龟兔赛跑的快慢指针方法来解决:
我们定义两个指针来指向这个链表的首个节点,当快指针进入环的时候慢指针可能还没进入,当快指针和慢指针全部进入环了之后,他们会在某个时刻相遇的。
如果没环,那fast就可以到NULL了。
代码实现:
bool hasCycle(struct ListNode* head)
{
struct ListNode* fast = head, * slow = head;
while (fast && fast->next)
//快指针是要每次走两步的,所以也要考虑fast->next是不是NULL
//1.防止野指针2.当fast->next->NULL时,也就相当于走到了尽头了。
{
slow = slow->next;
fast = fast->next->next;
if (slow == fast)
{
return true;
}
}
return false;
}
最后留一个思考题:快指针走3步可以吗?4步呢?n步呢?