机械攻城狮C++学习日记Ⅱ:链表理论及相关操作——应用向

本文介绍了链表的基本理论,包括单链表、双链表和循环链表的数据结构。详细阐述了链表的创建、初始化、销毁、长度统计以及插入、删除等常见操作。此外,还讨论了链表在实际问题中的应用,如翻转链表、两两交换节点、删除倒数第n个节点、查找链表相交点以及环形链表的检测和环入口的寻找。
摘要由CSDN通过智能技术生成

目录

1.链表理论

2.链表常用操作(以单链表为例)

3.链表应用

3.1翻转链表

3.2两两交换链表中的节点

3.3删除链表的倒数第n个节点

3.4链表相交

3.5环形链表

3.5.1判断链表是否有环

3.5.2寻找环的入口

4.总结


1.链表理论

        在线性表的链式描述中,线性表的元素在内存中的存储位置是随机的。每个元素都有一个明确的指针或链(指针和链是一个意思)指向线性表的下一个元素的位置,即地址。在基于数组的描述中,元素地址是由数学公式决定的。而在链式描述中,元素地址是随机分布的。在链式描述中,数据对象示例的每一个元素都用一个单元或节点描述,节点不必是数组成员,因此不是用公式来确定元素的位置。取而代之的是,每一个节点都明确包含另一个相关节点的位置信息,这个信息称为链或指针。

图1 单链表数据结构图

         链表类型包括单链表、双链表和循环链表。对于双链表,其每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点,其数据结构如图2所示。

图2 双向链表数据结构图

         循环链表是在单链表的基础上进行首尾相连,如图3所示。

图3 循环链表数据结构图

顺序表(数组描述)和链表的区别:

顺序表链表
存储空间预先分配,可能会导致空间闲置或溢出动态分配,不会出现空间闲置或者溢出
存储密度存储密度为1,逻辑关系等于存储关系,没有额外开销存储密度小于1,要借助指针域来表示元素之间的逻辑关系
存取元素随机存取,按位置访问元素的时间复杂度O(1)顺序存取,访问某位置的元素的时间复杂度为O(n)
插入、删除插入和删除都要移动大量的元素。平均移动元素约为表的一半。时间复杂度O(n)不需要移动元素,只需要改变指针位置,继而改变结点之间的链接关系。时间复杂度O(1)
适用情况1.表长变化不大,或者事先就能确定变化的范围 2.很少进行插入和删除,需要下标访问元素1.长度变化较大 2.频繁的插入和删除
这不就是vector的使用特点吗这不就是list的使用特点吗

2.链表常用操作(以单链表为例)

链表的定义:

struct Lnode
{
    ElemType data;//结点的数据域
    Lnode *next;//结点的指针域
};
typedef Lnode *LinkList;

链表的初始化:

bool InitList(LinkList &L)//插入题外话:LinkList &L等价于 Lnode *&L,Lnode *&L是一个指向指针的引用
{
    L = new Lnode; //堆区开辟一个头结点,结点的数据类型为Lnode
    L->next = nullptr;  //空表,也就是说头结点的指针指向为空
    return true;
}

头插法创建链表:

void CreatListHead(LinkList &L, const size_t n)
{
    for (int i = 0; i < n; ++i)
    {
        Lnode *p = new Lnode;
        cin >> p->data;
        p->next = L->next;
        L->next = p; 
    }
}

尾插法创建链表:

void CreatListTail(LinkList &L, const size_t n)
{
    Lnode *r = L;
    for (int i = 0; i < n; ++i)
    {
        Lnode *p = new Lnode;
        cin >> p->data;
        p->next = r->next;
        r->next = p;
        r = r->next;
    }
}

销毁链表:

bool DestroyList(LinkList &L)
{
    //判断链表是否为空
    if(IsEmpty(L))
    {
        cerr << "empty List!" << endl;
        return false;
    }
    while (L)//链表还未到达尾端
    {
        auto temp = L->next;//将头指针指向下一个结点
        delete L;
        L = temp;
    }
    return true;
}

统计链表长度:

size_t GetLength(const LinkList &L)
{
    Lnode *p;
    p = L->next;
    size_t cnt = 0;
    while (p)
    {
        ++cnt;
        p = p->next;
    }
    return cnt;
}
//算法的时间复杂度为O(n)

按值查找链表:

size_t LocateElem(LinkList &L, ElemType &e)
{
    Lnode *p = L->next;
    size_t cnt = 1;
    while (p)
    {
        if (p->data == e)
        {
            return cnt;
        }
        ++cnt;
        p = p->next;
    }
    cerr << "not found" << endl;
    return 0;
}

在链表中插入元素:

bool InsertList(LinkList &L, const int &i, const ElemType &e)
{
    Lnode *p = L;
    int j = 0;
    while(p && j < i-1)
    {
        p = p->next;
        ++j;
    }
    //异常判断
    if(!p || i<0)
    {
        cerr << "out of range" << endl;
        return false;
    }
    LinkList insert = new Lnode;
    insert->data = e;
    insert->next = p->next;
    p->next = insert;
    return true;
}
//算法的时间复杂度为O(n)

删除链表中的某个节点:

bool EraseList(LinkList &L, const int &i)
{
    Lnode *p = L;
    int j = 0;
    while (p->next && j < i - 1)
    {
        p = p->next;
        ++j;
    }
    if (!(p->next) || i < 0)
    {
        cerr << "out of range" << endl;
        return false;
    }
    Lnode *q = p->next;
    p->next = p->next->next;
    delete q;
    return true;
}

3.链表应用

3.1翻转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

图4 翻转链表示意

        最简单的方法是定义一个新的链表,然后将翻转后的链表储存到新链表中,不过这极大的浪费了内存。本道题使用了在数组题目中介绍的双指针法进行求解,首先定义一个cur指针,指向头结点,再定义一个pre指针,初始化为null。然后就要开始反转了,首先要把 cur->next 节点用tmp指针保存一下,也就是保存一下这个节点。为什么要保存一下这个节点呢,因为接下来要改变 cur->next 的指向了,将cur->next 指向pre ,此时已经反转了第一个节点了。接下来,就是循环走如下代码逻辑了,继续移动pre和cur指针。最后,cur 指针已经指向了null,循环结束,链表也反转完毕了。 此时我们return pre指针就可以了,pre指针就指向了新的头结点。

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode* temp; // 保存cur的下一个节点
        ListNode* cur = head;
        ListNode* pre = NULL;
        while(cur) {
            temp = cur->next;  // 保存一下 cur的下一个节点,因为接下来要改变cur->next
            cur->next = pre; // 翻转操作
            // 更新pre 和 cur指针
            pre = cur;
            cur = temp;
        }
        return pre;
    }
};

3.2两两交换链表中的节点

给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

        建议使用虚拟头结点,这样会方便很多,要不然每次针对头结点(没有前一个指针指向头结点),还要单独处理,主要交换步骤如图5。

图5 相邻节点交换
class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        ListNode* dummyHead = new ListNode(0); // 设置一个虚拟头结点
        dummyHead->next = head; // 将虚拟头结点指向head,这样方面后面做删除操作
        ListNode* cur = dummyHead;
        while(cur->next != nullptr && cur->next->next != nullptr) {
            ListNode* tmp = cur->next; // 记录临时节点
            ListNode* tmp1 = cur->next->next->next; // 记录临时节点

            cur->next = cur->next->next;    // 步骤一
            cur->next->next = tmp;          // 步骤二
            cur->next->next->next = tmp1;   // 步骤三

            cur = cur->next->next; // cur移动两位,准备下一轮交换
        }
        return dummyHead->next;
    }
};

3.3删除链表的倒数第n个节点

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

进阶:你能尝试使用一趟扫描实现吗

双指针的经典应用,如果要删除倒数第n个节点,让fast移动n步,然后让fast和slow同时移动,直到fast指向链表末尾。删掉slow所指向的节点就可以了。  

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummyHead = new ListNode(0);
        dummyHead->next = head;
        ListNode* slow = dummyHead;
        ListNode* fast = dummyHead;
        while(n-- && fast != NULL) {
            fast = fast->next;
        }
        fast = fast->next; // fast再提前走一步,因为需要让slow指向删除节点的上一个节点
        while (fast != NULL) {
            fast = fast->next;
            slow = slow->next;
        }
        slow->next = slow->next->next; 
        
        // ListNode *tmp = slow->next;  C++释放内存的逻辑
        // slow->next = tmp->next;
        // delete nth;
        
        return dummyHead->next;
    }
};

3.4链表相交

给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。

图示两个链表在节点 c1 开始相交

简单来说,就是求两个链表交点节点的指针。 这里注意,交点不是数值相等,而是指针相等。为了方便举例,假设节点元素数值相等,则节点指针相等。首先求出两个链表的长度,并求出两个链表长度的差值,然后让curA移动到和curB末尾对齐的位置,如图:

图6 链表相交

此时我们就可以比较curA和curB是否相同,如果不相同,同时向后移动curA和curB,如果遇到curA == curB,则找到交点。否则循环退出返回空指针。

//时间复杂度O(n + m)
//空间复杂度O(1)
class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        ListNode* curA = headA;
        ListNode* curB = headB;
        int lenA = 0, lenB = 0;
        while (curA != NULL) { // 求链表A的长度
            lenA++;
            curA = curA->next;
        }
        while (curB != NULL) { // 求链表B的长度
            lenB++;
            curB = curB->next;
        }
        curA = headA;
        curB = headB;
        // 让curA为最长链表的头,lenA为其长度
        if (lenB > lenA) {
            swap (lenA, lenB);
            swap (curA, curB);
        }
        // 求长度差
        int gap = lenA - lenB;
        // 让curA和curB在同一起点上(末尾位置对齐)
        while (gap--) {
            curA = curA->next;
        }
        // 遍历curA 和 curB,遇到相同则直接返回
        while (curA != NULL) {
            if (curA == curB) {
                return curA;
            }
            curA = curA->next;
            curB = curB->next;
        }
        return NULL;
    }
};

3.5环形链表

给定一个链表的头节点  head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改链表。

3.5.1判断链表是否有环

可以使用快慢指针法,分别定义 fast 和 slow 指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow 指针在途中相遇 ,说明这个链表有环。这是因为fast是走两步,slow是走一步,其实相对于slow来说,fast是一个节点一个节点的靠近slow的,所以fast一定可以和slow重合。

3.5.2寻找环的入口

假设从头结点到环形入口节点 的节点数为x。 环形入口节点到 fast指针与slow指针相遇节点 节点数为y。 从相遇节点 再到环形入口节点节点数为 z。 如图所示:

图7 寻找环入口变量定义

那么相遇时: slow指针走过的节点数为:x+y,fast指针走过的节点数:x+y+n(y+z),n为fast指针在环内走了n圈才遇到slow指针,(y+z)为一圈内节点的个数A。因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以fast指针走过的节点数 = slow指针走过的节点数 * 2:

注意这里n一定是大于等于1的,因为fast指针至少要多走一圈才能相遇slow指针。这个公式说明什么呢?先拿n为1的情况来举例,意味着fast指针在环形里转了一圈之后,就遇到了slow指针了。当 n为1的时候,公式就化解为x=z,这就意味着,从头结点出发一个指针,从相遇节点也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点。也就是在相遇节点处,定义一个指针index1,在头结点处定一个指针index2。让index1和index2同时移动,每次移动一个节点, 那么他们相遇的地方就是环形入口的节点。

图8 环形链表算法执行过程(图源自代码随想录)
class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        ListNode* fast = head;
        ListNode* slow = head;
        while(fast != NULL && fast->next != NULL) {
            slow = slow->next;
            fast = fast->next->next;
            // 快慢指针相遇,此时从head 和 相遇点,同时查找直至相遇
            if (slow == fast) {
                ListNode* index1 = fast;
                ListNode* index2 = head;
                while (index1 != index2) {
                    index1 = index1->next;
                    index2 = index2->next;
                }
                return index2; // 返回环的入口
            }
        }
        return NULL;
    }
};

4.总结

         对于链表,除了需要掌握增删改查等基本操作外,还需要学习例如虚拟头节点、双指针法等相关方法技巧,文中部分图片来源自代码随想录,后续会继续更新哈希表,如果觉得有用的话点个免费的赞吧!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Topom

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值