百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

数据结构与算法之链表的常用算法和应用场景

toqiye 2024-12-16 16:18 43 浏览 0 评论

这是我的读书笔记总结,关于链表的一些详细知识点并不会展开说明,有不懂的地方可以自行查阅相关资料

1. 本节知识点

  • 写好链表的一些技巧
  • 链表的简单算法实现
  • 基于链表实现LRU算法

2. 写好链表的一些技巧

  1. 理解指针或引用的含义
  2. 指针:将某个变量赋值给指针,实际上就是将这个变量的地址赋值给指针,或者反过来说,指针中存储了这个变量的内存地址,指向了这个变量,通过指针就能找到这个变量
  3. 警惕指针丢失和内存泄露
  4. 在添加节点或删除节点时,一定要注意顺序,否则会引起指针丢失
  5. 利用哨兵(头节点)简化实现难度
  6. 在操作链表时头部和尾部节点时,都需要特殊处理判断,如果使用哨兵节点,基本上可以避免上述操作
  7. 重点留意边界条件处理
  8. 举例说明,辅助思考
  9. 多写多练,没有捷径

3. 链表的简单算法实现

说明: 以下的代码 都是基于Go语言实现的相关算法,完整代码会在文章末尾贴出!

  1. 单链表反转
//定义节点
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 更新为当前节点,我们确保在下一次迭代时,能够正确地指向前一个节点,从而继续反转指向关系。这是链表翻转过程中的关键步骤之一

  1. 链表中环的检测
//使用快慢指针来实现,慢指针有一步,快指针有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步
    }
}
  1. 两个有序的链表合并
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 //链表的头部
    
}
  1. 删除链表倒数第 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,然后配合前端框...

取消回复欢迎 发表评论: