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


目录

🦄 链表的概念及结构

🦄 链表和顺序表的区别

🦄 单链表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;
}

双向链表

带头双向循环链表结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。 


基本的结构实现

 


初始化双向链表的头结点(Phead) 


尾插操作

而且通过双向链表也能够处理空链表的情况 


双向链表的打印函数


尾删操作 

注意保存要删除尾结点的前一个结点

 

但是这个代码存在一个问题:最后也会把哨兵结点删除

所以我们要多加一个判断


头删操作 

跟尾删一样,首先判断链表是否为空(为了保护哨兵结点


代码实现

LTNode* ListInit()
{
	// 哨兵位头结点
	LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
	phead->next = phead;
	phead->prev = phead;
}

void ListPushBack(LTNode* phead, LTDateType x)
{
	assert(phead);
	// 找尾结点
	LTNode* tail = phead->prev;
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	newnode->data = x;

	// 尾插操作
	tail->next = newnode;
	newnode->prev = tail;
	newnode->next = phead;
	phead->prev = newnode;
}

void ListPrint(LTNode* phead)
{
	assert(phead);

	LTNode* cur = phead->next;
	while (cur != phead)
	{
		print("%d ", cur->data);
		cur = cur->next;
	}
}

void ListPopBack(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);

	LTNode* tail = phead->prev;
	LTNode* tailPrev = tail->prev;
	free(tail);

	tailPrev->next = phead;
	phead->prev = tailPrev;
}

void ListPopFront(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);

	LTNode* next = phead->next;
	LTNode* nextNext = next->next;

	phead->next = nextNext;
	nextNext->prev = phead;

	free(next);
}

 

  • 15
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 12
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

玄澈_

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

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

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

打赏作者

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

抵扣说明:

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

余额充值