链表(Linked List)

什么是链表?

通过指针或者引用将一系列数据节点串起来的数据结构称为链表,链表数据结构与数组最明显的区别就是它在内存中并不是连续的,链表是通过在每个数据节点中设置下一个节点的指针Next将下一个节点串起来

为什么会有链表?

每一个编程语言中都会有数组的数据结构,可以直接通过索引下标来访问数组中的数据,所以对于数组来说访问一个值的时间复杂度为O(1)。但是如果要在数组中插入一个值时会有哪些情况呢

1、需要在数组的头插入
对于这种情况需要将数组中所有的元素往后挪动一步,然后再将待插入的数据插入第一个空间
2、需要在数组中间插入
其实这种情况跟第一种情况是一样的,只不过往后挪动的数据不是所有数据,只需要挪动待插入节点后面的数据
3、在数组的后面插入
这种情况最简单,只需要将待插入节点插入的最后即可

所以可以看到数组这种数据结构查询效率是非常高的,但是插入的效率并不高(数组元素的删除也是一样的,只不过挪动元素时是向前挪动而已);因为数组需要在内存中连续存储,所以如果插入时数组的空间不够时还需要动态扩容就更麻烦了;这时候我们的链表就闪亮登场了

什么情况下使用链表?

从前面可以看到数组的查询效率非常高效,但是插入跟删除时效率并不高;所以为了解决数组的插入和删除的效率低下就可以使用链表
所以如果你的数据是读多写少就选择用数组,相反如果你的数据需要大量的插入和删除,读的情况相对较少就可以使用链表来存储你的数据

下面是通过golang语言实现的常见链表的面试题
1、单链表反转
2、单链表相邻节点两两反转
3、检测链表是否有环
4、获取链表的中间节点
5、删除链表倒数第N个节点
6、合并两个有序链表
7、用链表实现LRU淘汰算法(最近最少使用淘汰算法)

package linkedlist

import (
    "fmt"
    "sync"
)

type Node struct {
    Value float64 // 这里的类型可以根据业务决定,我这里为了做有序链表合并定义了float64
    Next *Node
}

func (n* Node) String()string{
    if n == nil {
        return ""
    }

    ret := ""
    current := n
    for   {
        if current  == nil{
            ret += fmt.Sprint("nil")
            return ret
        }

        ret += fmt.Sprintf("%v->",current.Value)
        current = current.Next
    }
}

func NewNode(value float64)*Node{
    return &Node{
        Value:value,
    }
}

func NewNodeWithNext(value float64,next * Node)*Node  {
    return &Node{
        Value:value,
        Next:next,
    }
}

// 单链表反转
// input 1->2->3->4
// output 4->3->2->1
/**
 * 思路:pre:指向已经反转好的节点的头结点;current:指向下一个待转换的结点
 *
 * pre = nil, cur = 1->2->3->4->5->nil
 * pre = 1->nil, cur = 2->3->4->5->nil
 * pre = 2->1->nil, cur = 3->4->5->nil
 * pre = 3->2->1->nil, cur = 4->5->nil
 * ......
 * pre = 5->4->3->2->1->nil, cur = nil
 */
func Traverse(head * Node)*Node  {
    var  pre ,current *Node = nil,head
    for{
        if current == nil{
            return pre
        }
        current.Next,pre,current =pre,current,current.Next
    }
}

// 单链表相邻两个节点两两反转
// input 1->2->3->4->5
// output 2->1->4->3->5
/**
 * 思路:如果小于等于一个节点直接返回不需要反转
 *      这里必须要标记当前转换的两个节点的前一个节点,所以在刚开始时必须要构造这么一个节点
 *
 * pre = 2, cur = 0->1->2->3->4->5
 * pre = 2, cur = 0->2->1->3->4->5
 * pre = 2, cur = 0->2->1->4->3->5
 */
func Traverse2(head * Node)*Node{
    if head == nil || head.Next == nil {
        return head
    }

    pre := head.Next
    cur := NewNodeWithNext(0,head)
    for  {
        if cur.Next == nil || cur.Next.Next == nil{
            return pre
        }

        a := cur.Next
        b := cur.Next.Next
        cur.Next,a.Next,b.Next=b, b.Next,a
        cur = a
    }
}

// 检测链表是否有环(快慢指针)
func HasCircle(head * Node)bool{
    if head == nil{
        return false
    }

    slow,fast := head,head
    for   {
        if fast.Next == nil || fast.Next.Next == nil || slow.Next == nil {
            return false
        }

        fast = fast.Next.Next
        slow = slow.Next
        if slow == fast{
            return true
        }
    }
}

// 获取链表的中间节点
func GetMiddleNode(head * Node)* Node{
    if head == nil || head.Next == nil{
        return head
    }

    slow,fast := head,head
    for   {
        if fast.Next != nil && fast.Next.Next != nil && slow.Next != nil {
            slow = slow.Next
            fast=fast.Next.Next
            continue
        }

        return slow
    }
}

// 删除倒数第n个结点,如果倒数n个结点为头结点则不可删除
func RDelNode(head* Node,n int) * Node {
    slow,fast := head,head

    // 1 2 3 4 5
    for ; n > 0 ; n --   {
        if fast.Next == nil {
            break
        }
        fast = fast.Next
    }

    // 如果这里 n == 1 则表示删除头结点,目前可以考虑不删除头结点
    if n > 0 {
        return nil
    }

    for  {
        if fast.Next != nil && slow.Next != nil {
            fast = fast.Next
            slow = slow.Next
            continue
        }
        break
    }

    if slow != nil && slow.Next != nil{
        ret := slow.Next
        slow.Next = slow.Next.Next
        return ret
    }
    return nil
}

// 合并两个有序链表
/**
 * 思路:因为是有序的链表,所以有两种方式可以做
 * 1、重新开启一个链表,然后从两个待合并的链表的头中拿下来一个比较小的,放入新的链表中直到两个链表都为空
 * 2、上一个方式理解起来比较简单,但是会额外浪费m+n的空间;所以第二种方式就是一个开头较大的链表插入到开头较小的链表中即可
 */
func Merge(head1,head2 * Node)*Node{
    if head1 == nil{
        return head2
    }

    if head2 == nil{
        return head1
    }

    head,insert := head1,head2
    if head2.Value < head1.Value {
        head,insert = head2,head1
    }

    cur := head
    for {
        if insert == nil{
            return head
        }

        if cur.Next == nil {
            cur.Next = insert
            return head
        }

        for {
            if  insert.Value > cur.Value && cur.Next != nil && insert.Value < cur.Next.Value  {
                tmp := insert.Next
                insert.Next = cur.Next
                cur.Next = insert

                cur = cur.Next
                insert =tmp
                break
            }

            if cur.Next == nil {
                break
            }
            cur = cur.Next
        }
    }
}

// ------------------------**链表实现LRU淘汰算法(Least Recently Used)**------------------------------------
/**
 *  思路:
 * 1、如果当前数据在链表中存在则将当前数据提到链表头部
 * 2、如果在当前链表中不存在
 *  2.1、当前链表是否已满,当前数据替换链表中的最后一个节点
 *  2.2、当前链表没有满,将当前数据插入到链表的头部
 */
type LeastRecentlyUsed struct {
    Capacity uint64 // 容量
    Number uint64 // 当前链表数量
    Head  * Node // 链表头节点
    mu sync.RWMutex
}

func NewLeastRecentlyUsed(capacity uint64)*LeastRecentlyUsed{
    return &LeastRecentlyUsed{
        Capacity:capacity,
    }
}

// 返回要查找的结点的前一个节点跟自己的节点,如果pre为空则要查找的结点就是头结点
func (l * LeastRecentlyUsed)Find(value interface{})(pre,cur *Node,exist bool){
    l.mu.RLock()
    defer l.mu.RUnlock()

    if l.Head == nil {
        return
    }

    if l.Head.Value == value {
        cur = l.Head
        exist = true
        return
    }

    pre = l.Head
    for  {
        if pre.Next == nil {
            return
        }

        if pre.Next.Value == value {
            cur = pre.Next
            exist = true
            return
        }

        pre = pre.Next
    }
}

func (l * LeastRecentlyUsed)Use(value float64)  {
    pre,cur ,ok := l.Find(value)
    l.mu.Lock()

    // 如果当前节点已经存在则将当前节点放在第一个节点
    if ok && pre != nil{
        pre.Next= cur.Next
        cur.Next = l.Head
        l.Head = cur
        l.mu.Unlock()
        return
    }

    // 如果当前的缓存容量已经满了,则将当前节点覆盖最后一个节点
    if l.Capacity <= l.Number {
        if l.Capacity <= 1 {
            l.Head = NewNode(value)
            l.mu.Unlock()
            return
        }

        pre := l.Head
        for  {
            if pre.Next.Next != nil {
                pre = pre.Next
                continue
            }

            pre.Next = NewNode(value)
            l.mu.Unlock()
            return
        }

    }else{
        l.Number ++
        newNode := NewNodeWithNext(value,l.Head)
        l.Head = newNode
        l.mu.Unlock()
        return
    }
}

github地址:https://github.com/LiYanBing/golang-data-struct/tree/master/linkedlist

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,427评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,551评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,747评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,939评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,955评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,737评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,448评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,352评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,834评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,992评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,133评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,815评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,477评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,022评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,147评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,398评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,077评论 2 355

推荐阅读更多精彩内容