链表解决了哪些痛点?

86 阅读5分钟

糟糕的数组扩容

想象一下,你还是一名幽默风趣帅气多金的程序员,你的头发还是秃了,我们还是管你叫“秃头大帅”(whatever, who cares)。

这一天你戴着你发光的假发来到了一家4S店,你豪掷百金订了365辆不同的劳斯莱斯,你把它们按一定规则排好,希望新的一年每天开一辆天天不重样。

当你把这365辆劳斯莱斯开回家后,你发现你家的停车场太小了☹️。

于是你准备扩建你家的停车场,就像下面这样。扩容

86E9013B-D400-4C0B-AF42-E0CFF8FAE2CF.png

这时你体内的洪荒基因告诉你。

不要这样,不要这样,不要这样!

发明链表

于是你聪明的氢二氧再一次占据第4脑室,你思考片刻后,不费吹灰之力解决了这个问题。

很简单,你只是合理利用了你拥有的128个别墅的停车位,你把你的劳斯莱斯分布在这128个别墅的停车场上,你在每辆劳斯莱斯屁股上贴上接下来一天要开的莱斯莱斯。

这样,你得意的笑了笑,有钱人的生活就是这么朴实无华。

你给你的这个方案,起名为链表,英文名linked list,它的通用结构像下面这样。

AE714C3D-7D12-46D3-84C6-7F5611345EE3.png

你用下面的代码表示它

/* 链表节点结构体 */ 
type ListNode struct {     
    Val  int       // 节点值     
    Next *ListNode // 指向下一节点的指针 
}

链表的优势(相对于数组)

这一天你老婆花枝招展正好来找你,于是你顺便跟你老婆吹嘘了你一下你这个新方面,链表结构几方面的优势:

  1. ** 灵活性和扩展性:**链表允许你根据当前的需求动态地添加更多的存储空间(即停车场)。这避免了一开始就必须预留大量连续空间的要求。
  2. **高效的插入和删除:**当你需要在链表中插入或删除车辆(节点)时,你只需修改前后停车场(节点)的指向,而不需要移动大量的车辆(数据)。这在你频繁新购车或者卖车时特别有用。
  3. **节省空间:**只有在需要时才会增加新的停车场,这比起一开始就建一个大停车场来说,更加经济和高效。这样做不仅节省了资源,还能更灵活地应对不同的停车需求。

到这里,你老婆又对你投去了崇拜的目光。

你并未理会,你打算给你老婆深入讲一堂课《链表(你的新发明)》,包括链表的种类,增删(也即)操作,性能分析等

链表特性

新增节点(车子)

6B5AE3A3-C9B2-410B-B889-AE3F088A56DF.png

删除节点(车子)

2001BF3B-F622-4178-9B27-EC3F3B7E2640.png

性能分析

从上面可以看出,链表的操作性能

插入 O(1)

删除 O(1)

随机访问 O(n)

链表种类

  • 单向链表,从当前节点只能找到下一节点
  • 双向链表,从当前节点即能向后遍历,也可以向前遍历,方便,这下你可以方便的知道昨天开的什么车。

305FB987-4102-4CDD-8832-0E0C39CE047D.png

  • 环形链表,收尾相连,因果轮回、生生世世,你可以明年继续开。

3B849BB3-660C-48E7-B711-19543582D0EA.png

你又做了一下补充,对于链表的插入和删除,如果

讲到这里,你忻忻得意,你老婆情不自禁。

不过你并不满意,这个Moment,你心中又燃起了一团火,你感觉到,要爆了。

你咬了一口你老婆吃了还剩一半的苹果,掀开尘封多年的笔记本盖,默默的敲下了leetcode.com。

经典题解

2. 两数相加

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。

你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

考察链表遍历基础。

理解题目的关键点后,有以下思路:

同时遍历两个链表,把他们对应节点的值相加,不断插入新链表就可以了。可见这里其实是链表的插入问题,要处理的无非是进位的问题。

需要注意的是两个链表的长度可能不一致。

B6C2712F-8BC0-41B6-9C29-6C8F85DDE501.png

func addTwoNumbers(l1 *ListNode, l2 *ListNode) *ListNode {

	dummy := &ListNode{}
	result := dummy

	carry := 0 // 记录进位
	for l1 != nil && l2 != nil {
		x := l1.Val + l2.Val + carry
		carry = x / 10
		x = x % 10

		dummy.Next = &ListNode{Val: x}
		dummy = dummy.Next

		l1 = l1.Next
		l2 = l2.Next
	}

	for l1 != nil { // 如果l1长,继续遍历
		x := l1.Val + carry
		carry = x / 10
		x = x % 10
		dummy.Next = &ListNode{Val: x}
		dummy = dummy.Next

		l1 = l1.Next
	}

	for l2 != nil {
		x := l2.Val + carry
		carry = x / 10
		x = x % 10
		dummy.Next = &ListNode{Val: x}
		dummy = dummy.Next

		l2 = l2.Next
	}
	if carry >= 1 {
		dummy.Next = &ListNode{Val: carry}
		dummy = dummy.Next
	}
	return result.Next
}

19. 删除链表的倒数第 N 个结点

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

思路:

一般想法,先一趟计算长度length,然后再一遍遍历,找到第 lengthn+1个节点,执行删除。

一趟扫描:快慢指针,快指针先走,计数到第n个节点时,慢指针再走,快指针到结尾时,慢指针位置即要删除位置。

func removeNthFromEnd(head \*ListNode, n int) \*ListNode {

    dummy := \&ListNode{0, head}

    first, second := head, dummy

    for i := 0; i < n; i++ {

    first = first.Next

}

for ; first != nil; first = first.Next {

    second = second.Next

}

second.Next = second.Next.Next

    return dummy.Next

}