数据结构与算法之美笔记-链表(Linked list)

链表(Linked list)

缓存:

  • CPU 缓存
  • 数据库缓存
  • 浏览器缓存

缓存的大小有限,当缓存被用满时,哪些数据应该被清理出去,哪些数据应该被保留?

缓存淘汰策略:

  • 先进先出策略 FIFO(First In,First Out)
  • 最少使用策略 LFU(Least Frequently Used)
  • 最近最少使用策略 LRU(Least Recently Used)

链表不需要一块连续的内存空间,它通过"指针"将一组零散的内存块串联起来使用.

  • 单链表
  • 双链表
  • 循环链表
  • 双向循环链表

1. 单链表

结点:

  • 存储数据. data

  • 记录链上的下一个结点的地址. 后继指针 next

    –> (data, next) --> (data, next) --> (data, next) --> NULL

头结点: 第一个结点, 记录链表的基地址

尾结点: next 指向一个空地址 NULL, 表示这是链表上最后一个结点

链表的插入或删除时间复杂度是 O(1).

随机访问第 k 个元素, 需要从第一个一个向下找, 时间复杂度是 O(n)

2. 循环链表

尾结点: next 指向头结点

从链尾到链头比较方便

3. 双链表

  • 记录链上的上一个结点的地址. 前驱指针 prev

    –> (prev, data, next) <–> (prevdata, next) <–> (prevdata, next) <–> (prevdata, next) <–> (prevdata, next) --> NULL

删除操作

链表的删除操作时间复杂度是 O(1), 但是在实际的软件开发中, 删除往往需要先查找, 查找的时间复杂度是 O(n), 总的时间复杂度就是 O(n)

  • 删除结点中"值等于某个给定值"的结点
  • 删除给定指针指向的结点

对于 删除给定指针指向的结点, 单链表要从头查询一遍, 找到上一个结点, 把其 next 指向删除节点的下一个结点, 时间复杂度是 O(n). 双向链表直接就可以获取上一个结点, 时间复杂度是 O(1).

Java 的 LinkedHashMap 就是一个双向链表.

双向链表是一个空间换时间的设计思想.

缓存就是利用了空间换时间的设计思想。

  • 对于执行较慢的程序,可以通过消耗更多的内存(空间换时间)来进行优化;
  • 而消耗过多内存的程序,可以通过消耗更多的时间(时间换空间)来降低内存的消耗。

双向循环链表

ArrayList 也可以动态扩容, 但是需要数据拷贝, 非常耗时.

链表进行频繁的插入/删除操作, 会导致频繁的内存申请和释放, 容易造成内存碎片. 如果是 Java, 就有可能会导致频繁的 GC (Garbage Collection, 垃圾回收).

4. 如何基于链表实现 LRU 缓存淘汰算法?

维护一个有序单链表,越靠近链表尾部的结点是越早之前访问的。当有一个新的数据被访问时,我们从链表头开始顺序遍历链表。

  1. 如果此数据之前已经被缓存在链表中了,遍历得到这个数据对应的结点,并将其从原来的位置删除,然后再插入到链表的头部。
  2. 如果此数据没有在缓存链表中,又可以分为两种情况:
    • 如果此时缓存未满,则将此结点直接插入到链表的头部;
    • 如果此时缓存已满,则链表尾结点删除,将新的数据结点插入链表的头部。

不管缓存有没有满,都需要遍历一遍链表,所以这种基于链表的实现思路,缓存访问的时间复杂度为 O(n)。

可以继续优化, 例如收入散列表 (Hash table) 记录每个数据的位置, 时间复杂度降至 O(1).

5. 课后思考

如果字符串是通过单链表来存储的,那该如何来判断是一个回文串呢

  1. 快慢指针定位中间节点
  2. 从中间节点对后半部分逆序
  3. 前后半部分比较,判断是否为回文
  4. 后半部分逆序复原

时间复杂度 O(n), 空间复杂度 O(1).

技巧一:理解指针或引用的含义

将某个变量赋值给指针,实际上就是将这个变量的地址赋值给指针,或者反过来说,指针中存储了这个变量的内存地址,指向了这个变量,通过指针就能找到这个变量。

技巧二:警惕指针丢失和内存泄漏

  • 插入结点时,一定要注意操作的顺序
  • 删除链表结点时,也一定要记得手动释放内存空间

技巧三:利用哨兵简化实现难度

针对链表的插入、删除操作,需要对插入第一个结点和删除最后一个结点的情况进行特殊处理。

如果引入哨兵结点,在任何时候,不管链表是不是空,head 指针都会一直指向这个哨兵结点。把这种有哨兵结点的链表叫带头链表。相反,没有哨兵结点的链表就叫作不带头链表

技巧四:重点留意边界条件处理

经常用来检查链表代码是否正确的边界条件有这样几个:

  • 如果链表为空时,代码是否能正常工作?
  • 如果链表只包含一个结点时,代码是否能正常工作?
  • 如果链表只包含两个结点时,代码是否能正常工作?
  • 代码逻辑在处理头结点和尾结点的时候,是否能正常工作?

技巧五:举例画图,辅助思考

技巧六:多写多练,没有捷径

  • 单链表反转
  • 链表中环的检测
  • 两个有序的链表合并
  • 删除链表倒数第 n 个结点
  • 求链表的中间结点

Golang 链表实现

type Link interface {
	IsEmpty() bool
	Length() int
	Contain(data interface{}) bool
	Add(data interface{})
	Append(data interface{})
	Insert(index int, data interface{})
	Delete(data interface{}) bool
	DeleteAt(index int) bool

	Reverse()
	IsHasRing() bool

	String() string
	Print()
}

Golang 单链表:

import (
	"bytes"
	"fmt"
)

type SingleNode struct {
	Data interface{}
	Next *SingleNode
}

type SingleLink struct {
	header *SingleNode
}

// IsEmpty 是否为空链表
func (self *SingleLink) IsEmpty() bool {
	return self.header == nil
}

// Length 长度
func (self *SingleLink) Length() int {
	count, cur := 0, self.header
	for cur != nil {
		count++
		cur = cur.Next
	}
	return count
}

// Contain 是否包含
func (self *SingleLink) Contain(data interface{}) bool {
	cur := self.header
	for cur != nil {
		if cur.Data == data {
			return true
		}
		cur = cur.Next
	}
	return false
}

func (self *SingleLink) String() string {
	buf := bytes.NewBufferString("")
	cur := self.header
	for cur != nil {
		// TODO 优化
		buf.WriteString(cur.Data.(string))
		cur = cur.Next
		if cur != nil {
			buf.WriteString("-")
		}
	}
	return buf.String()
}

func (self *SingleLink) Print() {
	if self.IsHasRing() {
		fmt.Println("链表存在环, 无法打印")
	} else {
		fmt.Println(self.String())
	}
}

// Add 头部添加
func (self *SingleLink) Add(data interface{}) {
	node := &SingleNode{
		Data: data,
		Next: self.header,
	}
	self.header = node
}

// Append 尾部添加
func (self *SingleLink) Append(data interface{}) {
	node := &SingleNode{
		Data: data,
	}
	if self.IsEmpty() {
		self.header = node
	} else {
		cur := self.header
		for cur.Next != nil {
			cur = cur.Next
		}
		cur.Next = node
	}
}

// Insert 指定位置添加
func (self *SingleLink) Insert(index int, data interface{}) {
	if index == 0 {
		self.Add(data)
	} else {
		node := &SingleNode{
			Data: data,
		}
		cur := self.header
		if cur == nil {
			cur = &SingleNode{}
		}
		index--
		for index > 0 {
			if cur.Next == nil {
				cur.Next = &SingleNode{}
				cur = cur.Next
			}
			index--
		}
		node.Next = cur.Next
		cur.Next = node
	}
}

// Delete 删除数据
func (self *SingleLink) Delete(data interface{}) bool {
	pre := self.header
	if pre == nil {
		return false
	}
	if pre.Data == data {
		self.header = pre.Next
		return true
	}
	cur := pre.Next
	for cur != nil {
		if cur.Data == data {
			pre.Next = cur.Next
			return true
		}
		cur = cur.Next
	}
	return false
}

// DeleteAt 删除指定位置数据
func (self *SingleLink) DeleteAt(index int) bool {
	pre := self.header
	if pre == nil {
		return false
	}
	if index == 0 {
		self.header = pre.Next
		return true
	}
	cur := pre.Next
	index--
	for cur != nil && index > 0 {
		pre = cur
		cur = cur.Next
		index--
	}
	if cur != nil {
		pre.Next = cur.Next
		return true
	}
	return false
}

func (self *SingleLink) Reverse() {
	pre := self.header
	if pre == nil || pre.Next == nil {
		return
	}
	cur := pre.Next
	pre.Next = nil
	for cur != nil {
		tmp := cur.Next
		cur.Next = pre
		pre = cur
		cur = tmp
	}
	self.header = pre
}

// IsHasRing 是否存在环
func (self *SingleLink) IsHasRing() bool {
	slow := self.header
	fast := self.header
	for fast != nil && fast.Next != nil && fast.Next.Next != nil {
		slow = slow.Next
		fast = fast.Next.Next
		if slow == fast {
			return true
		}
	}
	return false
}

// GetAt 根据索引获取指定位置的节点
func (self *SingleLink) GetNodeAt(index int) *SingleNode {
	cur := self.header
	for cur != nil && index > 0 {
		cur = cur.Next
		index--
	}
	return cur
}

// Merge 两个有序的链表合并
func (self *SingleLink) Merge(list2 *SingleLink) *SingleLink {
	header := &SingleNode{}
	link := &SingleLink{header: header}
	node1 := self.header
	node2 := list2.header
	for node1 != nil || node2 != nil {
		// TODO 优化, 暂时以字符串处理
		if node1 != nil && (node2 == nil || node1.Data.(string) < node2.Data.(string)) {
			header.Next = node1
			header = header.Next
			node1 = node1.Next
		} else if node2 != nil && (node1 == nil || node2.Data.(string) < node1.Data.(string)) {
			header.Next = node2
			header = header.Next
			node2 = node2.Next
		}
	}
	link.DeleteAt(0)
	return link
}

func Test(link Link) {
	link.Print()
}

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