数据结构算法——单链表及其操作

单链表是一种非常常用的数据结构,虽然很简单,但是其相关操作还是很容易出错。本文将介绍单链表的几种操作,主要包括链表的反转,链表的排序,求出链表倒数第 k 个值,删除当前节点和找出链表的中间节点。
更多内容可参考如下链接:

https://www.61mon.com/index.php/archives/179/


单链表节点结构

定义单链表的节点结构如下:

/* 单链表节点结构 */
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;
}

你可能感兴趣的:(数据结构与算法)