《数据结构之美--链表oj练习》
链表oj题分享
1. 移除链表元素
题目:
思路分析:
根据题目描述,可以看出该题是要将满足条件的链表元素删除,并且返回新的头结点. 首先我们想到的肯定是直接遍历该链表然后对满足条件的元素进行删除,但删除某个元素时还会影响前后节点,因此还需要修改前后元素的指针,但显然这样就显得很麻烦,我们不妨换一种角度来看这个问题,既然题目要求我们删除满足条件的元素,也就是保留不满足条件的元素,为此,我们是不是可以搞一个新的链表,然后从前往后遍历原链表,只要是不满足条件的元素就尾插到新链表之后,这样就避免了一些不方便的操作,为此给出实现方案。
代码实现:
我们根据思路一气呵成写出了上面的代码,乍一看是没什么问题,但提交之后就会发现出错了。
这里我们可以看出这个测试用例是要求我们把val值为6的节点删除,但我们还是输出了最后一个满足条件的节点,咦?这是为什么呢?我们再仔细地检查一遍代码,就不难发现在处理到倒数第二个节点时,让newtail
指针指向了该节点,然后最后一个节点满足删除条件,因此我们没有将其插入到新链表中,接着就返回了新的头结点,但是我们忽略了一个细节,newtail
指针的next
指针并未置为空,它还是指向最后一个节点,但是最后一个节点我们是不要的啊,因此,只需要在返回头结点之前,将newtail
的next
指针置为空即可
可以看到,在特判尾节点之后我们这道题就AC了
题目传送门 :
203–移除链表元素: link
2. 反转链表
题目:
思路分析:
这道题要求很简单,就是将链表反转一下,然后返回新的头结点,因此我们很容易想出一种思路:就是创建一个新链表(脑海中),然后从前往后遍历原链表,不断进行头插操作。
这里我再来提供另一种思路 : 定义三个指针 n1
n2
n3
让n1
置为空,n2
指向头结点
n3
指向头结点的下一个节点,然后n2不为空的情况下不断更新三个指针的值,让n2
的next指针 指向 n1
, n1
指向 n2
,n2
指向 n3
,只要n3
不为空就往后走,跳出循环时n1
就指向原链表的最后一个节点。
下面画图来分析这种方法
画图分析
代码实现:
方法一:
这里的思路虽然简单,但是需要注意的一点就是你在修改指针的时候会修改pcur
的next
指针,但之后你还要走到下一个节点的位置,但是此时next
指针已经被修改了,因此在修改指针之前需要定义一个中间指针来暂存next
指针。
方法二:
这里需要注意的一个细节就是n3
指针在往后走的时候要注意判断是否为空,以防对空指针的解引用.
题目传送门
206-反转链表:链接: link
3. 链表的中间节点
题目 :
思路分析 :
通过阅读题目,这道题目就是要求我们返回一个链表中的中间节点,因此我们脑海中很快就产生了一个思路,如果我们知道链表中节点的个数,然后直接返回中间的节点不就可以了,这个思路也容易实现,只需要将链表遍历一遍就可以统计出链表的节点个数。
这里再分享另一种思路:快慢指针
定义两个指针:slow
和 fast
,一开始都让它们指向头结点,之后遍历链表,slow
指针每次走一步,fast
指针每次走两步,当fast
指针走到尾节点时循环结束,此时slow
指针指向的就是中间节点。
画图分析
代码实现:
方法一:
方法二:
这里唯一需要的就是while循环中的两个条件不能交换位置,因为有可能会出现对空指针的解引用,这时就会报错。
题目传送门:
876-链表的中间节点-: link
4. 合并有序链表
题目:
思路分析:
读完题目,可以发现这道题目就是要让我们把两个链表按升序合并,这里注意很重要的一个点(两个有序序列),其实这道题和之前我们做过的合并两个有序数组很相似,这也是归并排序的一个核心思想,因此我们只需用合并两个有序数组的逻辑来实现就行,(定义两个指针遍历两个链表,每次将较小的节点尾插到新链表当中,由于两个链表肯定会有一个先遍历完,接着只需将剩下的链表挂到新链表之后即可,因为两个链表都是有序的)。
代码实现:
初始代码:
typedef struct ListNode ListNode;
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
//特判空链表情况
if(list1 == NULL)
{
return list2;
}
if(list2 == NULL)
{
return list1;
}
ListNode* newhead,*newtail;
newhead = newtail = NULL;
ListNode* cur1, *cur2;
cur1 = list1,cur2 = list2;
while(cur1 && cur2)
{
if(cur1->val <= cur2->val)
{
//尾插
if(newhead == NULL)
{
newhead = newtail = cur1;
}
else
{
newtail->next = cur1;
newtail = cur1;
}
cur1 = cur1->next;
}
else
{
//尾插
if(newhead == NULL)
{
newhead = newtail = cur2;
}
else
{
newtail->next = cur2;
newtail = cur2;
}
cur2 = cur2->next;
}
}
//因为肯定会有一个链表先遍历完,接着把没遍历完的那个链表直接挂到尾部即可
if(cur1)
{
newtail->next = cur1;
newtail = newtail->next;
}
if(cur2)
{
newtail->next = cur2;
newtail = newtail->next;
}
return newhead;
}
唯一需要注意的就是链表为空的情况,这时候要判空
虽然这个代码实现很简单,但尾插部分的代码显得太过冗余了,而且我们还需要判断链表是否为空,这样很麻烦,因此这里我们其实可以创建一个带头链表,之后往头结点之后尾插节点,这样就省去了判空的操作,下面给出优化的代码
优化代码:
typedef struct ListNode ListNode;
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
//特判空链表情况
if(list1 == NULL)
{
return list2;
}
if(list2 == NULL)
{
return list1;
}
ListNode* p = (ListNode*)malloc(sizeof(ListNode));
ListNode* newhead,*newtail;
newhead = newtail = p;
ListNode* cur1, *cur2
;
cur1 = list1,cur2 = list2;
while(cur1 && cur2)
{
if(cur1->val <= cur2->val)
{
newtail->next = cur1;
newtail = cur1;
cur1 = cur1->next;
}
else
{
newtail->next = cur2;
newtail = cur2;
cur2 = cur2->next;
}
}
//因为肯定会有一个链表先遍历完,接着把没遍历完的那个链表直接挂到尾部即可
if(cur1)
{
newtail->next = cur1;
newtail = newtail->next;
}
if(cur2)
{
newtail->next = cur2;
newtail = newtail->next;
}
return newhead->next;
}
这里需要注意两个点:
- 我们返回的头结点是
newhead ->next
,因为头结点使我们自己申请的 - 由于我们申请了一个节点,在最后的时候需要释放掉。
题目传送门
21-合并两个有序链表-: link
5. 链表回文
题目:
思路分析:
思路一
根据之前解决回文数和回文字符串的经验来解决这道题。
将链表的所有节点存入一个数组中,然后定义一前一后两个指针,不断地比较两个位置的节点是否相同。
思路二
先找到链表的中间节点,将中间节点作为新链表的头结点进行反转,然后定义两个指针,一个从原链表的头结点开始,另一个指针从原链表的尾节点开始,不断比较节点是否相同
画图分析
代码实现:
方法一:
方法二:
这里注意我们在判断是否回文的时候,while循环中的条件是以右边指针不为空。
题目传送门:
234-回文链表-: link
6. 相交链表
题目:
思路分析:
读完题目,可以知道题目是要求我们判断两个链表是否相交,判断两个链表是否相交,其实就是判断两个链表是否存在相同的地址,那我们不是定义两个指针来同时遍历两个链表,然后每走一步就判断两个节点是否相同不就可以了吗?但我们拿示例来模拟得话就会发现,当两个链表长度不相等时就会出错。因此我们就需要固定两个指针的出发点,让较长的那个链表的指针先走几步,之后就可以正确地来判断了。
代码实现:
题目传送门:
160-相交链表-: link
7. 环形链表
题目:
思路分析:
通过题目描述,我们知道这道题目就是要求我们来判断一个链表是否有环,乍一看没什么思路,其实这道题也可以用到之前的快慢指针,如果链表带环的话,那么快慢指针一定会相遇,否则说明链表不带环。
画图分析:
代码实现:
题目传送门:
141-环形链表-: link
8. 环形链表||
题目:
思路分析:
这道题相比上道题单纯地判断链表是否带环,只是多了一个如果带环的话要求我们返回入环的起始节点,上道题我们是通过快慢指针来判断是否带环,如果带环的话快慢指针一定会相遇,也就是说我们可以找到快慢指针相遇的节点,那么我们知道这个有什么用呢? 其实这里存在着一个很重要的性质:相遇点到入环起始点的距离与头结点到入环起始节点的距离是相等的
既然存在这个性质,我们就可以在找到快慢指针相遇点之后,定义两个指针,一个从头结点开始,一个从相遇点开始,同时进行遍历,这两个指针的相遇点就是我们要找的入环起始点。
讲到这里可能会有同学怀疑这个性质的正确性,下面我们来证明一下:
证明:
代码实现:
题目传送门:
142-环形链表||-: link
总结:
这篇文章主要是和大家分享一些简单的单链表oj题来巩固一下学习的单链表,上面几道题如果有更优的算法欢迎随时和我交流分享。