IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 数据结构与算法 -> 【玄说?数据结构与算法】【初阶】—— 链表 -> 正文阅读

[数据结构与算法]【玄说?数据结构与算法】【初阶】—— 链表


目录

🦄?链表的概念及结构

🦄?链表和顺序表的区别

🦄?单链表OJ

?链表的概念及结构

概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。?

?大致的链表结构如下:


链表的打印函数?

接下来通过链表的打印函数来进一步理解链表的结构?

void SListPrint(SLTNode* phead)
{
	SLTNode* cur = phead;
	while(cur != NULL)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
}

?


?由上述的打印函数可得,判断最后一个元素的判断依据是:next指针为NULL

链表的尾部插入元素函数

接下来我们实现在尾部插入一个元素

我们需要先找到尾结点,然后再接入一个新的结点

我们可以先写出来下面的代码:

对于我们上面写出的代码,我们用一下程序来进行测试:

然后我们可以发现,程序崩溃了? ??

?通过调试我们可以发现,我们最开始的链表头结点是NULL,我们通过这个头结点来进行查找尾结点的时候,会出现错误。

针对这个问题,我们需要进行一些优化(多一步判断条件)

同时还有一个问题,我们传入的是?int* phead,?在我们的函数中,它本质上还是一个形参,没有办法修改到原来的指针,所以,我们需要传入指针的指针,即?int** phead?才能真正修改原来的指针

void SListPushBack(SLTNode** pphead, SLTDateType x)
{
	// 创建新插入元素的结点
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	newnode->next = NULL;
	newnode->data = x;

	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		// 找到尾结点
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}

		//插入元素
		tail->next = newnode;
	}
	
}

链表的头部插入元素函数

为了方便生成结点,我们写了一个?BuyList() 的函数

然后进一步实现头插函数,先将新结点指向头结点的下一个结点,再将新结点接到头结点后面

我们可以发现,在头插的情况下,就就算是空链表也不会出现问题,所以不需要多一步的判断操作?


删除操作(头删+尾删)

由于单链表无法找到前置元素,所以需要用类似双指针的方法来保留前一个元素的地址

但是上述代码还存在问题,当链表元素的个数只有一个或者是为空时,该代码会运行崩溃

void SListPopBack(SLTNode** pphead)
{
	// 温柔的方法
	if (*pphead == NULL)
	{
		return;
	}
	// 强硬的方法
	assert(*pphead != NULL);

	// 只有一个结点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLTNode* tail = *pphead;
		SLTNode* pre = *pphead;
		while (tail->next != NULL)
		{
			pre = tail;
			tail = tail->next;
		}
		free(tail);
		pre->next = NULL;
	}
}

头删操作?


?查找函数的实现

这是查找函数的主体,但是当链表中有多个相同的数据的时候,我们该如何一一查找出来呢?

如下图所示:

通过循环来实现相同数据的查找!💨💨


在指定位置插入元素函数

找到pos位置的前置元素,然后在prev的后面插入新的元素即可

但是通过实例可以发现,当我们要在第一个元素之前插入时,上述代码会运行出错,此时需要我们多一步判断操作(头插)


链表和顺序表的区别

顺序表

缺陷:

  1. 空间不够了,需要扩容,扩容是有消耗
  2. 头部或者是中间的位置的插入删除,需要挪动,挪动数据也是需要有消耗的
  3. 避免频繁扩容,一次一遍都是按照倍数去扩容(2倍),可能存在一定的空间浪费

优点:

  1. 可以使用下标来访问:a[i] 等价于 *(a + i)(支持随机访问)


链表

优点:

  1. 按需申请空间,不用了就释放空间(更合理地使用了空间)
  2. 头部或者是中间插入删除数据,不需要挪动数据(不存在空间浪费)

?缺点:

  1. 每一个数据,都需要存一个指针去链接后面的数据结点
  2. 不支持随机访问(用下标访问第 i 个)

单链表OJ题

LeetCode 203. 移除链表元素

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */


struct ListNode* removeElements(struct ListNode* head, int val){
    if(head == NULL) return NULL;

    struct ListNode* cur = head;
    struct ListNode* prev = head;
    while(cur != NULL)
    {
        if(cur->val == val)
        {
            // 删除
            // 头删
            if(cur == head)
            {
                head = cur->next;
                free(cur);
                cur = head;
            }
            else
            {
                prev->next = cur->next;
                free(cur);
                cur = prev->next;
            }
        }
        else
        {
            // 迭代往后走
            prev = cur;
            cur = cur->next;
        }
    }
    return head;
}

LeetCode?206. 反转链表


?直接在原来的链表进行修改,修改每个结点的next指针

?需要定位指针来帮助我们定位地址

struct ListNode* reverseList(struct ListNode* head){
    if(head == NULL) return NULL;
    struct ListNode* n1, *n2, *n3;
    n1 = NULL;
    n2 = head;
    n3 = head->next;

    while(n2)
    {
        // 翻转
        n2->next = n1;

        // 迭代往后走
        n1 = n2;
        n2 = n3;
        if(n3) n3 = n2->next;
    }

    return n1;
}

头插法不断取出新的元素头插到新链表中

struct ListNode* reverseList(struct ListNode* head){
    struct ListNode* cur = head;
    struct ListNode* newhead = NULL;
    while(cur)
    {
        struct ListNode* next = cur->next;
        // 头插
        cur->next = newhead;
        newhead = cur;

        // 迭代往后走
        cur = next;
    }

    return newhead;
}

?LeetCode876. 链表的中间结点


这道题可以用到经典的?“快慢指针”?算法


struct ListNode* middleNode(struct ListNode* head){
    struct ListNode* slow, *fast;
    slow = fast = head;
    while(fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
    }
    return slow;
}

链表中倒数第k个结点?


  1. fast 先走k步
  2. slow 和 fast 一起走,fast == NULL 时就是倒数第 k 个?

struct ListNode* FindKthToTail(struct ListNode* pListHead, int k ) {
    if(pListHead == NULL) return NULL;
    
    struct ListNode* fast, *slow;
    slow = fast = pListHead;
    while(k -- )
    {
        if(fast == NULL)
        {
            return NULL;
        }
        fast = fast->next;
    }
    
    while(fast)
    {
        slow = slow->next;
        fast = fast->next;
    }
    return slow;
}

LeetCode??????21. 合并两个有序链表


struct ListNode* mergeTwoLists(struct ListNode* l1, struct ListNode* l2){
    if(!l1) return l2;
    if(!l2) return l1;

    struct ListNode* head = NULL, *tail = NULL;
    while(l1 && l2)
    {
        if(l1->val < l2->val)
        {
            if(head == NULL)
            {
                head = tail = l1;
            }
            else
            {
                tail->next = l1;
                tail = l1;
            }
            l1 = l1->next;
        }
        else
        {
             if(head == NULL)
            {
                head = tail = l2;
            }
            else
            {
                tail->next = l2;
                tail = l2;
            }
            l2 = l2->next;
        }
    }
    if(l1)
    {
        tail->next = l1;
    }
    if(l2)
    {
        tail->next = l2;
    }

    return head;
}

链表分割_牛客题霸_牛客网

class Partition {
public:
    ListNode* partition(ListNode* pHead, int x) {
        struct ListNode* lessHead, *lessTail, *greaterHead, *greaterTail;
        // 开一个哨兵位头结点,方便尾插
        lessHead = lessTail = (struct ListNode*)malloc(sizeof(struct ListNode*));
        lessTail->next = NULL;
        greaterHead = greaterTail = (struct ListNode*)malloc(sizeof(struct ListNode*));
        greaterTail->next = NULL;
        
        struct ListNode* cur = pHead;
        while(cur)
        {
            if(cur->val < x)
            {
                lessTail->next = cur;
                lessTail = cur;
            }
            else
            {
                greaterTail->next = cur;
                greaterTail = cur;
            }
            cur = cur->next;
        }
        
        lessTail->next = greaterHead->next;
        greaterTail->next = NULL;
        return lessHead->next;
    }
};

链表的回文结构_牛客题霸_牛客网


一种方法是利用前半个链表的元素之和减去后半个链表的元素之和,判断其是否为0

(要注意区分奇数情况和偶数情况)

class PalindromeList {
public:
    bool chkPalindrome(ListNode* A) {
        int count = 0;
        int sum = 0;
        struct ListNode* cur = A;
        while(cur)
        {
            count ++ ;
            cur = cur->next;
        }
        if(count % 2 == 0)
        {
            int k = 0;
            cur = A;
            while(cur)
            {
                k ++ ;
                if(k <= count / 2) sum += cur->val;
                else sum -= cur->val;
                
                cur = cur->next;
            }
            if(sum == 0) return true;
            else return false;
        }
        else
        {
            int k = 0;
            cur = A;
            while(cur)
            {
                k ++ ;
                if(k == count / 2 + 1) ;
                else if(k <= count / 2) sum += cur->val;
                else if(k > count / 2 + 1) sum -= cur->val;
                
                cur = cur->next;
            }
            if(sum == 0) return true;
            else return false;
        }
    }
};

?另外一种解法,是先找到链表的中间结点,然后将后半部分的链表逆置,然后与前半部分的链表一一对比

struct ListNode* middleNode(struct ListNode* head){
    struct ListNode* slow, *fast;
    slow = fast = head;
    while(fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
    }
    return slow;
}

struct ListNode* reverseList(struct ListNode* head){
    struct ListNode* cur = head;
    struct ListNode* newhead = NULL;
    while(cur)
    {
        struct ListNode* next = cur->next;
        // 头插
        cur->next = newhead;
        newhead = cur;

        // 迭代往后走
        cur = next;
    }

    return newhead;
}
class PalindromeList {
public:
    bool chkPalindrome(ListNode* A) {
        struct ListNode* mid = middleNode(A);
        struct ListNode* rHead = reverseList(mid);
        
        struct ListNode* curA = A;
        struct ListNode* curR = rHead;
        while(curA && curR)
        {
            if(curA->val != curR->val)
            {
                return false;
            }
            else
            {
                curA = curA->next;
                curR = curR->next;
            }
        }
        return true;
    }
};

LeetCode160. 相交链表


思路1: 暴力求解(穷举法)?

依次取出A链表中的每个结点跟B链表中的所有节点比较

如果有地址相同的点,就是相交,第一个相同的结点

O(n^2)

思路2:O(n)?的解法

1.尾结点相同就是相交

2.求交点长的链表先走(长度差)步,再同时走,第一个相交的点就是交点


struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    struct ListNode* tailA = headA;
    struct ListNode* tailB = headB;
    int lenA = 0, lenB = 0;
    while(tailA->next)
    {
        tailA = tailA->next;
        lenA ++ ;
    }

    while(tailB->next)
    {
        tailB = tailB->next;
        lenB ++ ;
    }

    // 不相交
    if(tailA != tailB)
    {
        return NULL;
    }
    // 长的先走差距步
    int gap = abs(lenA - lenB);
    struct ListNode* longList = headA;
    struct ListNode* shortList = headB;
    if(lenA < lenB)
    {
        longList = headB;
        shortList = headA;
    }

    while(gap -- )
    {
        longList = longList->next;
    }

    while(longList != shortList)
    {
        longList = longList->next;
        shortList = shortList->next;
    }
    return longList;
}

LeetCode141. 环形链表?


思路:【快慢指针】?

快慢指针,即慢指针一次走一步,快指针一次走两步,两个指针从链表其实位置开始运行,如果链表带环则一定会在环中相遇,否则快指针率先走到链表的末尾。

bool hasCycle(struct ListNode *head) {
    struct ListNode* slow = head, *fast = head;
    while(fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
        if(slow == fast) return true;
    }
    return false;
}

【拓展问题】

  • 为什么快指针每次走两步,慢指针走一步可以?
    • 假设链表带环,两个指针最后都会进入环,快指针先进环,慢指针后进环。当慢指针刚进环时,可能就和快指针相遇了最差情况下两个指针之间的距离刚好就是环的长度。此时,两个指针每移动一次,之间的距离就缩小一步,不会出现每次刚好是套圈的情况,因此:在满指针走到一圈之前,快指针肯定是可以追上慢指针的,即相遇。
  • 快指针一次走3步,走4步,...n步行吗?
    • ?如果是距离是偶数的情况,差距为奇数步(如上图)则不会相遇
    • ?

?LeetCode142. 环形链表 II

?


【结论】?

????????让一个指针从链表起始位置开始遍历链表,同时让一个指针从判环时相遇点的位置开始绕环运行,两个指针都是每次均走一步,最终肯定会在入口点的位置相遇

【证明】???


struct ListNode *detectCycle(struct ListNode *head) {
    struct ListNode* fast = head, *slow = head;
    while(fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
        if(slow == fast) 
        {
            // 相遇
            struct ListNode* meet = slow;
            while(meet != head)
            {
                meet = meet->next;
                head = head->next;
            }

            return meet;
        }
    }
    return NULL;
}

?

  数据结构与算法 最新文章
【力扣106】 从中序与后续遍历序列构造二叉
leetcode 322 零钱兑换
哈希的应用:海量数据处理
动态规划|最短Hamilton路径
华为机试_HJ41 称砝码【中等】【menset】【
【C与数据结构】——寒假提高每日练习Day1
基础算法——堆排序
2023王道数据结构线性表--单链表课后习题部
LeetCode 之 反转链表的一部分
【题解】lintcode必刷50题<有效的括号序列
上一篇文章      下一篇文章      查看所有文章
加:2022-07-20 19:09:52  更:2022-07-20 19:10:55 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/25 23:11:33-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码