数据结构与算法之链表的常用算法和应用场景
toqiye 2024-12-16 16:18 43 浏览 0 评论
这是我的读书笔记总结,关于链表的一些详细知识点并不会展开说明,有不懂的地方可以自行查阅相关资料
1. 本节知识点
- 写好链表的一些技巧
- 链表的简单算法实现
- 基于链表实现LRU算法
2. 写好链表的一些技巧
- 理解指针或引用的含义
- 指针:将某个变量赋值给指针,实际上就是将这个变量的地址赋值给指针,或者反过来说,指针中存储了这个变量的内存地址,指向了这个变量,通过指针就能找到这个变量
- 警惕指针丢失和内存泄露
- 在添加节点或删除节点时,一定要注意顺序,否则会引起指针丢失
- 利用哨兵(头节点)简化实现难度
- 在操作链表时头部和尾部节点时,都需要特殊处理判断,如果使用哨兵节点,基本上可以避免上述操作
- 重点留意边界条件处理
- 举例说明,辅助思考
- 多写多练,没有捷径
3. 链表的简单算法实现
说明: 以下的代码 都是基于Go语言实现的相关算法,完整代码会在文章末尾贴出!
- 单链表反转
//定义节点
type Node struct {
Value int
Next *Node
}
//定义链表
type LinkedList struct {
Header *Node
}
//单链表反转
func (l *LinkedList) ReverseList(){
var prev *Node //定义一个空节点
curNode :=l.Header //定义当前节点
for curNode !=nil { //循环链表
next :=curNode.Next //定义下一个节点
curNode.Next=prev //当前的下一个节点指向prev(反转)
prev=curNode //更新prev 为当前节点
curNode=next //更新当前节点为下一个节点
}
l.Header=prev //将 Header节点更新为prev,没有这一步,反转后还是和以前一样,由于指针丢失,所以只会打印第一个值,不信你试试
}
看不懂?没关系
让我们通过一个具体的例子来说明这一点:
假设我们有一个链表 1 -> 2 -> 3 -> nil,现在我们要将它翻转。
初始时,prev 是 nil,current 是指向节点 1 的指针。
第一次迭代:
保存当前节点的下一个节点,即 next = 2。
将当前节点的 Next 指针指向 prev,即 1 -> nil。
更新 prev 为当前节点,即 prev = 1。
更新 current 为下一个节点,即 current = 2。
第二次迭代:
保存当前节点的下一个节点,即 next = 3。
将当前节点的 Next 指针指向 prev,即 2 -> 1。
更新 prev 为当前节点,即 prev = 2。
更新 current 为下一个节点,即 current = 3。
第三次迭代:
保存当前节点的下一个节点,即 next = nil(链表末尾)。
将当前节点的 Next 指针指向 prev,即 3 -> 2。
更新 prev 为当前节点,即 prev = 3。
更新 current 为下一个节点,即 current = nil。
循环结束,最终链表变为 3 -> 2 -> 1 -> nil。
通过将 prev 更新为当前节点,我们确保在下一次迭代时,能够正确地指向前一个节点,从而继续反转指向关系。这是链表翻转过程中的关键步骤之一
- 链表中环的检测
//使用快慢指针来实现,慢指针有一步,快指针有2步,只要存在环,快指针会在某个时刻与慢指针重合,
// 如果不存在环,慢快指针会最先到达链表尾
func (l *LinkedList) HasCycle() bool {
header :=l.Header
//如果链表为空,则直接返回
if header ==nil || header.Next==nil{
return false
}
slow :=header
fast :=header.Next
for slow !=fast {
if fast ==nil || fast.Next==nil{
return false
}
slow=slow.Next
fast=fast.next.next //快指针走2步
}
}
- 两个有序的链表合并
func (l *LinkedList) MerageTwoList(l1 *Node,l2 *Node){
//定义头节点
h := *Node{}
//定义当前节点
cur :=h
for l1 !=nil && l2 !=nil{
if l1.value<l2.value{
cur.next=l1
l1=l1.Next
}else{
cur.Next=l2
l2=l2.Next
}
//更新当前节点为下一个节点
cur=cur.Next
}
if l1 !=nil{
cur.Next=l1
}
if l2 !=nil{
cur.Next=l2
}
h.Next=cur.Next //链表的头部
}
- 删除链表倒数第 n 个结点
//最主要的问题,是如何定位到要删除的节点?
//利用快慢指针实现,先让快指针走n+1个位置,在同时移动快慢指针,
//直到快指针指向链表尾时,慢指针指向删除节点的前一位置
func (l *LinkedList) removeNthFromEnd(n int) {
h :=l.Header
slow :=h
fast :=h
//移动快指针对n+1处
for i=0;i<=n;i++{
fast=fast.Next
}
//同时移动快慢指针
for fast !=nil{
slow=slow.Next
fast=fast.Next
}
//到这里 slow指针所在有位置是 删除节点的前一个位置
slow.Next=slow.Next.Next
}
举个例子说明下:
假设我们有一个链表:1 -> 2 -> 3 -> 4 -> 5,要删除倒数第 2 个节点(节点值为 4)。
首先,我们创建一个哑节点 dummy,并将其 Next 指向链表的头节点,以便处理边界情况。此时,链表变为:dummy -> 1 -> 2 -> 3 -> 4 -> 5。
接下来,我们需要让 fast 指针先向前移动 n+1 步。在这个例子中,n 是 2,所以 fast 先向前移动 3 步,到达节点 3。此时,fast 指针在节点 3,slow 指针在哑节点 dummy。
现在,我们让 slow 和 fast 指针同时向前移动,直到 fast 到达链表末尾。这是因为我们需要让 slow 指针停在要删除节点的前一个节点上。在这个例子中,fast 指针最终会到达节点 5,而 slow 指针会在节点 3 停下。
最后,我们将 slow 指针的 Next 指向倒数第 n+1 个节点的 Next,从而跳过了倒数第 n 个节点。在这个例子中,slow.Next 指向节点 5 的 Next,即节点 4 被删除。这样就完成了删除倒数第 2 个节点的操作。
删除后的链表:1 -> 2 -> 3 -> 5。
4. 基于链表实现LRU算法
说明:
这里使用双链表和hash表来实现,使用的库是container/list go内置库,对go不熟悉的同学,可以看看思路即可
//定义LRU结构体
//list.Element node节点
type LRUCache struct {
capacity int
cache map[int]*list.Element
lruList *list.List
}
//缓存的key value 这里只支持int类型 用在生产环境上,意义不大, 可以使用泛型来实现更完善的算法
type CahceItem struct {
key int
value int
}
//生成对象,就是初始化各个参数
func NewLRUCache(n int) *LRUCache {
return &LRUCache{
capacity: n,
cache: make(map[int]*list.Element),
lruList: list.New(),
}
}
//获取缓存中的值
func (this *LRUCache) Get(key int) int {
//判断key是否存在
if e, ok := this.cache[key]; ok {
//如里存在,则移动链表头部
this.lruList.MoveToFront(e)
//可以看做类型断言 如果为真,则返回转化后的值
return e.Value.(*CahceItem).value
}
return -1
}
func (this *LRUCache) Set(key, value int) {
if el, ok := this.cache[key]; ok {
//如里存在,则更新对应的value,放到链表头部
el.Value.(*CahceItem).value = value
this.lruList.MoveToFront(el)
} else {
//不存在
//判断是不是满了,如里满了就删除最后一位
if len(this.cache) >= this.capacity {
//取出最后一个元素
lastEm := this.lruList.Back()
//删除hash表中的数据
delete(this.cache, lastEm.Value.(*CahceItem).key)
//删除链表中的节点
this.lruList.Remove(lastEm)
}
//放到链表头部
newEl := this.lruList.PushFront(&CahceItem{key: key, value: value})
//放入到hash表中
this.cache[key] = newEl
}
}
5.完整的代码,如下 可运行
package linkdemo
import (
"container/list"
"fmt"
"testing"
)
type Node struct {
Value int
Next *Node
}
type LinkedList struct {
Header *Node
}
// 添加链表
func (l *LinkedList) AddNode(n int) {
newNode := &Node{
Value: n,
Next: nil,
}
if l.Header == nil {
l.Header = newNode
} else {
curNode := l.Header
for curNode.Next != nil {
curNode = curNode.Next
}
//
curNode.Next = newNode
}
}
func (l *LinkedList) PrintList() {
curNode := l.Header
for curNode != nil {
fmt.Println(curNode.Value, " ")
curNode = curNode.Next
}
}
// 翻转链表
func (l *LinkedList) ReverseList() {
var prev *Node
curNode := l.Header
for curNode != nil {
next := curNode.Next //下个节点
curNode.Next = prev //将当前节点为的下一节点指向 prev
prev = curNode //更新prev 为当前节点
curNode = next //更新当前节点为下一个节点
}
l.Header = prev
}
// 检测链表中是否存在环
func (l *LinkedList) HasCycle() bool {
header := l.Header
if header == nil || header.Next == nil {
return false
}
slow := l.Header
fast := l.Header.Next
for slow != fast {
if fast == nil || fast.Next == nil {
return false
}
slow = slow.Next
fast = fast.Next.Next
}
return true
}
func (l *LinkedList) MerageList(l1 *Node, l2 *Node) {
h := &Node{} //定义头节点
cur := h
for l1 != nil && l2 != nil {
if l1.Value <= l2.Value {
cur.Next = l1
l1 = l1.Next
} else {
cur.Next = l2
l2 = l2.Next
}
cur = cur.Next
}
if l1 != nil {
cur.Next = l1
}
if l2 != nil {
cur.Next = l2
}
l.Header = h.Next // 链表的头部
}
func (l *LinkedList) removeNthFromEnd(n int) {
h := l.Header
slow := h
fast := h
//先把快指针移动到要删除节点的前一位
for i := 0; i <= n; i++ {
fast = fast.Next
}
//同时移到快慢指针
for fast != nil {
slow = slow.Next
fast = fast.Next
}
slow.Next = slow.Next.Next
}
func (l *LinkedList) findMiddleNode() *Node {
slow, fast := l.Header, l.Header
for fast != nil && fast.Next != nil {
slow = slow.Next
fast = fast.Next.Next
}
return slow
}
// 链表翻转
func TestLinkReverse(t *testing.T) {
list := LinkedList{}
list.AddNode(1)
list.AddNode(2)
list.AddNode(3)
list.AddNode(4)
list.AddNode(5)
list.PrintList()
list.ReverseList()
fmt.Println("---")
list.PrintList()
}
func TestHasCycle(t *testing.T) {
h := &LinkedList{}
node1 := &Node{Value: 1}
node2 := &Node{Value: 2}
node3 := &Node{Value: 3}
node4 := &Node{Value: 4}
node5 := &Node{Value: 5}
h.Header = node1
node1.Next = node2
node2.Next = node3
node3.Next = node4
node4.Next = node5
//node5.Next = node2 //构造一个环
hasCycle := h.HasCycle()
if hasCycle == true {
t.Error("存在环")
} else {
t.Error("不存在环")
}
}
func TestMerageTwoLink(t *testing.T) {
l1 := &Node{
Value: 1,
Next: &Node{
Value: 3,
Next: &Node{
Value: 5,
Next: nil,
},
},
}
l2 := &Node{
Value: 1,
Next: &Node{
Value: 3,
Next: &Node{
Value: 4,
Next: nil,
},
},
}
l := &LinkedList{}
l.MerageList(l1, l2)
l.PrintList()
}
func TestRemoveNthFromEnd(t *testing.T) {
list := &LinkedList{}
list.AddNode(1)
list.AddNode(2)
list.AddNode(3)
list.AddNode(4)
list.AddNode(5)
list.PrintList()
list.removeNthFromEnd(2)
fmt.Println("=====")
list.PrintList()
}
func TestFindMiddleNode(t *testing.T) {
list := &LinkedList{}
list.AddNode(1)
list.AddNode(2)
list.AddNode(3)
list.AddNode(4)
list.AddNode(5)
list.AddNode(6)
node := list.findMiddleNode()
fmt.Println(node)
}
type LRUCache struct {
capacity int
cache map[int]*list.Element
lruList *list.List
}
type CahceItem struct {
key int
value int
}
func NewLRUCache(n int) *LRUCache {
return &LRUCache{
capacity: n,
cache: make(map[int]*list.Element),
lruList: list.New(),
}
}
func (this *LRUCache) Get(key int) int {
if e, ok := this.cache[key]; ok {
//如里存在,则移动链表头部
this.lruList.MoveToFront(e)
return e.Value.(*CahceItem).value
}
return -1
}
func (this *LRUCache) Set(key, value int) {
if el, ok := this.cache[key]; ok {
//如里存在,则更新,放到链表头部
el.Value.(*CahceItem).value = value
this.lruList.MoveToFront(el)
} else {
//不存在
//判断是不是满了,如里满了就删除最后一位
if len(this.cache) >= this.capacity {
lastEm := this.lruList.Back()
delete(this.cache, lastEm.Value.(*CahceItem).key)
this.lruList.Remove(lastEm)
}
newEl := this.lruList.PushFront(&CahceItem{key: key, value: value})
this.cache[key] = newEl
}
}
func TestLRU(t *testing.T) {
cache := NewLRUCache(2)
cache.Set(1, 1)
cache.Set(2, 2)
g := cache.Get(1)
fmt.Println(g)
cache.Set(3, 3)
g2 := cache.Get(2)
fmt.Println(g2)
}
相关推荐
- Star 17.3k!给它一张屏幕截图,即可一键克隆网页!
-
本文为大家分享一款本周爆火的GPT开源项目。前言你敢信,只凭借着一张屏幕截图即可转换生成HTML/TailwindCSS代码。可以算得上是前端工程师的福音。它就是screenshot-to-...
- AI从截图直接生成代码、前端程序员的福利!
-
简介项目可以将任何屏幕截图或设计转换为干净的代码(支持大多数框架)。来自领先公司的开发人员和设计师使用的排名第一的工具。完全开源,在GitHub上拥有超过35,000颗星。非常受欢迎。各位小伙...
- 一款高颜值、跨平台、自托管的免费开源CRM项目——Twenty
-
前言大家好,这里是可爱的Cherry。作为一个“甲方”,Cherry其实挺知道客户管理的重要的。但是对于客户管理怎么做,以及CRM的作用,我却是一无所知。之前有朋友在评论区留言,说有没有开源的CRM系...
- 解放双手,前端界面再也不用自己写了?
-
随着AI技术的发展,现在有越来越多的尝试将AI应用于UI设计和开发中,以期提高效率和降低成本。今天就给大家介绍一个开源的AI网页生成工具:OpenUIOpenUIOpenUI是一个创...
- 代码调试,教给你(代码调试是什么意思)
-
昨天我和一些朋友一起调试代码,他们做程序员这一行都不太久,我向他们展示了一些代码调试技巧。今天早上我在想,我应该如何教授他们学习代码调试?我在Twitter上发了一条推文说,我从来没有见过任何好的调试...
- Screenshot-to-code:用屏幕截图生成代码
-
Screenshot-to-code是一个简单的工具,可使用AI将屏幕截图、模型和Figma设计转换为干净、实用的代码。现在支持ClaudeSonnet3.5和GPT-4o!Scre...
- next实现原理(next method)
-
Next.js是一个基于React的服务器端渲染(SSR)和静态生成(SSG)框架,它的实现原理涉及多个关键技术点,包括服务端渲染(SSR)、静态生成(SSG)、客户端渲染(CSR)、...
- 可逐步操作的具体流程(可逐步操作的具体流程包括)
-
假设你是一个单人开发者,使用主流技术栈(React+Node.js+MySQL),以下是详细步骤:---###**一、需求分析与原型设计**1.**核心功能清单**-用户能添加、删除、...
- 截图转代码只需1步!你离高效开发只差这款神器
-
引言在现代前端开发中,将设计稿转换为代码是一个既重要又耗时的环节。手动编写HTML结构、调试CSS样式、调整布局对齐,不仅耗费时间,还容易出错。而Screenshot-to-Code这款革...
- web开发 前端 后端(web开发前端后端)
-
区别:1、前端是指用户可见的界面,而后端是指用户看不到的东西,考虑底层业务逻辑的实现,平台的稳定性、性能等。2、前端开发用到的技术有HTML5、CSS3、JS、jQuery、Bootstrap、Nod...
- 手把手教你Dify私有化部署,打造专属AI平台
-
一、Dify是什么?Dify是一款极具创新性的开源LLM应用开发平台,它就像是一把万能钥匙,为开发者们打开了通往生成式AI应用开发新世界的大门。其融合了后端即服务(BackendasS...
- 前后端分离架构设计:提升开发效率与业务支撑力的密钥
-
前后端分离架构设计解析一、定义与核心思想前后端分离是一种将用户界面(前端)与业务逻辑(后端)解耦的架构模式,通过RESTfulAPI或GraphQL实现数据交互。前端专注于视图渲染与交互逻辑...
- Kubernetes最小部署单元Pod(kubernetes最小部署单元)
-
一、Kubernetes与Pod简介在当今云计算和容器化技术盛行的时代,Kubernetes已然成为容器编排领域的中流砥柱。它是一个开源的容器编排平台,由Google基于其内部使用的Bo...
- 【程序员必藏!零基础本地部署DeepSeek大模型保姆级教程】
-
为什么选择本地部署?数据安全:敏感代码/业务数据永不外传闪电响应:局域网推理延迟<100ms,告别云端排队深度定制:自由修改模型代码,打造专属AI助手准备工具(5分钟搞定)1核心工具下载...
- 【Python程序开发系列】使用Flask实现前后端分离(案例)
-
这是我的第398篇原创文章。一、引言随着web开发的不断发展,前后端分离已成为越来越流行的架构设计。Flask是一个轻量级的Pythonweb框架,非常适合用于构建API,然后配合前端框...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- minorgc (62)
- systemproperties (77)
- vue3-template-admin (63)
- electronsqlite3 (65)
- npm版本管理 (61)
- materialtheme (86)
- node-ssh (68)
- 图床搭建 (62)
- vue3addeventlistener (60)
- mybatisselectone (78)
- css圆形进度条 (69)
- androidble蓝牙开发 (62)
- android-gif-drawable (60)
- appender-ref (64)
- springbootmockito (68)
- 依赖注入的方式 (62)
- cookie跨域共享 (63)
- easyexcel导出图片 (77)
- dp数组 (61)
- js获取兄弟节点 (68)
- sysctl-a (60)
- window.target (62)
- apimodel注解的作用 (60)
- window.onerror (66)
- springmvc教程 (65)