单链表是一种非常常用的数据结构,虽然很简单,但是其相关操作还是很容易出错。本文将介绍单链表的几种操作,主要包括链表的反转,链表的排序,求出链表倒数第 k 个值,删除当前节点和找出链表的中间节点。
更多内容可参考如下链接:
单链表节点结构
定义单链表的节点结构如下:
/* 单链表节点结构 */
struct Node
{
int data;
Node * next;
Node() { data = 0; next = nullptr; }
};
/* 定义头节点 */
Node * header = new Node;
链表反转
链表反转是面试笔试中非常常见的题型,需要注意的是反转过程中不能出现中断的情况。
示例:1->2->3->4->5; 反转后:5->4->3->2->1
反转过程中两个指针同步移动,具体代码如下:
/* 反转链表 */
void reverse(Node * header)
{
if(!header->next || !header->next->next)//空链表或只有一个节点
return;
Node* cur = header->next;//指向链表第一个节点
Node* pre = nullptr;
while(cur)
{
Node* tmp = cur->next;//保存下一节点
cur->next = pre;//更改节点指向
pre = cur;//pre向前移动一步
cur = tmp;//cur向前移动一步
}
header->next = pre;//头节点指向反转后的第一个节点
}
链表的排序
排序采用快速排序的思想,只需要设置两个指针 i 和 j,这两个指针均往 next 方向移动,移动的过程中始终保持区间 [1, i] 的 data 都小于 base(位置 0 是主元),区间 [i+1, j) 的 data 都大于等于 base,那么当 j 走到末尾的时候便完成了一次支点的寻找。
/**
* 链表升序排序
*
* begin 链表的第一个结点,即 header->next
* end 链表的最后一个结点的 next
*/
void asc_sort(Node * begin, Node * end)
{
if(!begin || !begin->next)// 链表为空或只有一个节点
return;
int base = begin->data;// 设置主元
Node* i = begin; // i 左边的小于 base
Node* j = begin->next; // i 和 j 中间的大于 base
while(j!=end)
{
if(j->data<base)
{
i = i->next;
swap(i->data, j->data);
}
swap(i->data, begin->data); // 交换主元和 i 的值
}
//
asc_sort(begin, i); // 递归左边
asc_sort(i->next, end); // 递归右边
}
//使用方式
asc_sort(header->next, nullptr);
求出链表倒数第 k 个值
由于链表长度未知,如果从头扫描一遍链表后得到链表长度,然后再从头找到倒数第k个数,这种方法最直观,但是时间复杂度较高。我们假设 k 小于等于链表长度,那么我们可以设置两个指针 p 和 q,这两个指针在链表里的距离就是 k,那么后面那个指针走到链表末尾的 nullptr 时,另一个指针肯定指向链表倒数第 k 个值。
/* 返回链表倒数第k个值 */
int kth_last(Node * header, int k)
{
Node* p = header->next;
Node* q = header->next;
for(int i=0;i<k;i++)
{
if(!q)
return -1; //链表长度小于k
q = q->next;
}
while(q)
{
p = p->next;
q = q->next;
}
return p->data;
}
删除当前节点
要求删除节点的时间平均复杂度为O(1),这样就不能通过扫描链表获取当前节点的前一个节点的方法了。既然指针不能从当前节点的上一个节点指向当前节点的下一个节点,那么就改变当前节点的值为下一个节点的值,同时将下一个节点删除。需要注意的是删除最后一个节点的情况,因为没有下一个节点,故需要找到上一个节点,同样需要扫描链表。
/* 删除当前结点 */
void del(Node * header, Node * position)
{
if(!position->next) // 要删除的是最后一个节点
{
Node* p = header;
while(p->next!=position)
p = p->next; // 找到 position 的前一个结点
p->next = nullptr;
delete position;
}
else
{
Node* p = position->next;
swap(position->data, p->data);
position->next = p->next;
delete p;
}
}
找出链表的中间节点
通过寻找倒数第k个数的操作后,我们可以想到用类似的两个指针进行操作,这里就采用快慢指针,慢指针走的长度等于快慢指针相距的程度,所以利用这个性质,当快指针走到链表尾时,慢指针正好在中间结点。
/* 找出单链表的中间结点 */
Node * find_middle(Node * header)
{
Node* p = header;
Node* q = p;
while(q->next->next && p->next)
{
p = p->next; // 慢指针走一步
q = q->next->next; // 快指针走两步
}
return p;
}