★★★★★个人专栏《C语言》** 《数据结构-初阶》 ★★★★★**
欢迎各位大佬交流!!!
通过对经典链表OJ题目的练习,不仅能加深对链表的理解,更能体会链表的精妙之处!
目录
5、环形链表的约瑟夫问题
看完题目之后,显然这是环形链表问题;
首先创建n个节点的链表,并且尾节点的next指向头节点;
接着开始报数,一直到最后一个人,返回其val;
一、创建链表
为了保证链表的存在,我们必须使用malloc去开辟一块空间,创建头指针;
那么尾指针需不需要malloc?
不需要!!!
创建对应的结构体指针即可,初始化时将tial指向head
cpp
struct ListNode* head = (struct ListNode*)malloc(sizeof(struct ListNode));
head->val = 1;
head->next = NULL;
struct ListNode* tail = head;
接着创建剩下的节点
cpp
for (int i = 2; i <= n; i++)
{
struct ListNode* newnode = (struct ListNode*)malloc(sizeof(struct ListNode));
newnode->val = i;
newnode->next = NULL;
tail->next = newnode;
tail = newnode;
}
跳出循环后,将tail的next指向head,这样就完成了循环链表的构建;
最终返回head
二、处理报数
首先需要创建cur指针用来遍历循环链表;
当报数为m时,需要删除当前节点,删除当前节点需要当前节点的前一个节点;
因此我们在创建prev,但是怎样进行初始化呢?
直接将prev = cur吗?
那这样还需要额外的判断,不如将创建链表函数返回tail指针,这样prev和cur均能正常赋值
接着就是进行判断,如果当前节点报数为m,就删除当前节点,否则就将prev、cur均向前走一步;
循环结束的条件就是cur->next == cur,此时表示链表中仅有一个元素
最终直接返回cur->val即可
cpp
struct ListNode* CreateList(int n)
{
//先创建一个节点
struct ListNode* head = (struct ListNode*)malloc(sizeof(struct ListNode));
head->val = 1;
head->next = NULL;
struct ListNode* tail = head;
//再创建剩下的节点
for (int i = 2; i <= n; i++)
{
struct ListNode* newnode = (struct ListNode*)malloc(sizeof(struct ListNode));
newnode->val = i;
newnode->next = NULL;
tail->next = newnode;
tail = newnode;
}
tail->next = head;
return tail;
}
int ysf(int n, int m)
{
// write code here
//创建环形链表
struct ListNode* prev = CreateList(n);
struct ListNode* cur = prev->next;
int cnt = 1;
while (cur->next != cur)
{
while(cnt != m)
{
cnt++;
prev = cur;
cur = cur->next;
}
cnt = 1;
struct ListNode* next = cur->next;
prev->next = next;
free(cur);
cur = next;
}
return cur->val;
}

6、分割链表
思路一:
很容易想到新建一个链表,将小于x的进行头插,大于等于x的进行尾插;
首先创建出虚拟头节点,尾指针以及遍历原链表所需的cur指针
接着就开始遍历原链表
如果cur->val < x,就进行头插,但第一次头插时,需要更新tail的指向,将tail改为指向cur节点的指针;
后续在头插操作中便不再需要更新tail,因为头插之后,新链表的尾指针也不变;
如果cur->val >= x,就得进行尾插,尾插是否也需要对第一次进行特殊处理呢?并不需要!
因为尾插之后尾节点自动更新成新链表中的最后一个节点;
最后,一定要记得将尾指针的next置为空,否则程序就会超时
cpp
struct ListNode* partition(struct ListNode* head, int x)
{
//思路一:新建链表,小于x头插,大于等于x尾插
struct ListNode* cur = head;
struct ListNode* dummyhead = (struct ListNode*)malloc(sizeof(struct ListNode));
struct ListNode* tail = dummyhead;
dummyhead->next = NULL;
while(cur)
{
struct ListNode* next = cur->next;
if(cur->val < x)
{
if(tail == dummyhead)
{
//第一次头插,更改尾节点
tail = cur;
}
//头插到新链表中
cur->next = dummyhead->next;
dummyhead->next = cur;
}
else
{
//尾插即可
tail->next = cur;
tail = cur;
}
cur = next;
}
if(tail != NULL)
tail->next = NULL;
return dummyhead->next;
}

思路二:
思路一中我们创建了一个链表,实现了对链表的分割;
那如果大胆一点,直接创建两个链表会怎么样?
小链表用来处理cur->val < x 这种情况;直接尾插到小链表中
大链表用来处理cur->val >= x 这种情况,直接尾插到大链表中
最后将小链表的尾节点与大链表的头节点相连,再将大链表的尾节点的next置为空;
返回小链表的head即可
实际上思路二的代码要比思路一简单很多
cpp
//创建两个链表
struct ListNode* smallhead = (struct ListNode*)malloc(sizeof(struct ListNode));
struct ListNode* smalltail = (struct ListNode*)malloc(sizeof(struct ListNode));
smallhead = smalltail = NULL;
struct ListNode* bighead = (struct ListNode*)malloc(sizeof(struct ListNode));
struct ListNode* bigtail = (struct ListNode*)malloc(sizeof(struct ListNode));
bighead = bigtail = NULL;
struct ListNode* cur = head;
while(cur)
{
struct ListNode* next = cur->next;
if(cur->val < x)
{
//尾插到小链表中
if(smalltail == NULL)
{
//第一次尾插
smallhead = smalltail = cur;
}
else
{
//直接尾插
smalltail->next = cur;
smalltail = cur;
}
}
else
{
//尾插到大链表中
if(bigtail == NULL)
{
bighead = bigtail = cur;
}
else
{
//直接尾插
bigtail->next = cur;
bigtail = cur;
}
}
cur = next;
}
此时,我们需要连接两个链表
在连接前一定要进行判空,那我们直接提前处理;
如果小链表的尾指针为空,说明原链表的每个节点的val均 >= x,因此直接返回大链表的头指针即可
反之,直接返回小链表的头指针即可;
如果没有返回,那就连接两个链表,最终返回小链表的头节点
cpp
struct ListNode* partition(struct ListNode* head, int x)
{
//创建两个链表
struct ListNode* smallhead = (struct ListNode*)malloc(sizeof(struct ListNode));
struct ListNode* smalltail = (struct ListNode*)malloc(sizeof(struct ListNode));
smallhead = smalltail = NULL;
struct ListNode* bighead = (struct ListNode*)malloc(sizeof(struct ListNode));
struct ListNode* bigtail = (struct ListNode*)malloc(sizeof(struct ListNode));
bighead = bigtail = NULL;
struct ListNode* cur = head;
while(cur)
{
struct ListNode* next = cur->next;
if(cur->val < x)
{
//尾插到小链表中
if(smalltail == NULL)
{
//第一次尾插
smallhead = smalltail = cur;
}
else
{
//直接尾插
smalltail->next = cur;
smalltail = cur;
}
}
else
{
//尾插到大链表中
if(bigtail == NULL)
{
bighead = bigtail = cur;
}
else
{
//直接尾插
bigtail->next = cur;
bigtail = cur;
}
}
cur = next;
}
//连接两个链表
if(smalltail == NULL) return bighead;
if(bigtail == NULL) return smallhead;
smalltail->next = bighead;
bigtail->next = NULL;
return smallhead;
}

7、返回倒数第k个节点
这道题如果之前没做过的话,我觉得不太好想;
同样使用快慢指针,快指针先走差距步,也就是先走k步,然后再同时走;
当快指针走到为空时,慢指针就走到了倒数第k个节点的位置
最终返回慢指针的val即可
cpp
int kthToLast(struct ListNode* head, int k)
{
struct ListNode* fast = head;
struct ListNode* slow = head;
//fast先走差距步
while(k--)
{
fast = fast->next;
}
//再同时走
while(fast)
{
slow = slow->next;
fast = fast->next;
}
return slow->val;
}

8、链表的回文结构
看到时间复杂度为O(n),额外空间复杂度为O(1),表明我们只能遍历一遍链表,并且只能创建变量,无法使用辅助数组等;
那么我们先找到链表的中间节点,其实也就是在《经典链表OJ-上》中出现过的题;
然后将以链表中间节点为头节点的链表进行逆置;
假如以1-2-3-2-1为例;
逆置之后由于前半段的2节点的下一个节点是3节点,那么正好符合一一对应;
第一步前半段1和后半段1进行比较;
第二步前半段2和后半段2进行比较;
第三步前半段3和后半段3进行比较;
再走一步,就走到空,循环结束
另一个1-2-1-2偶数节点链表和上述同理
当链表节点为偶数个时,rmid就会先走到空;
因此循环结束的条件就是只要A或者rmid有一个为空就直接结束;

总结一下:
a、先找到中间节点
b、接着逆置以中间节点为头节点的链表
c、进行比较
cpp
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};*/
class PalindromeList {
public:
struct ListNode* middleNode(struct ListNode* head)
{
struct ListNode* slow = head,*fast = head;
while(fast && fast->next)
{
fast = fast->next->next;
slow = slow->next;
}
return slow;
}
struct ListNode* reverseList(struct ListNode* head)
{
//逆置从head开始的链表
struct ListNode* cur = head;
struct ListNode* prev = NULL;
while(cur)
{
struct ListNode* next = cur->next;
cur->next = prev;
prev = cur;
cur = next;
}
//cur的前一个元素即为逆置链表后的头节点
return prev;
}
bool chkPalindrome(ListNode* A)
{
// write code here
struct ListNode* mid = middleNode(A);
struct ListNode* rmid = reverseList(mid);
while(rmid && A)
{
if(rmid->val != A->val) return false;
rmid = rmid->next;
A = A->next;
}
return true;
}
};
如有不足之处恳请指出!!!
谢谢大家!!!