链表

概述

链表(linked list)是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存储到下一个节点的指针

特性

单链表

  • 每个节点都知道它下一个节点的地址
  • 元素不存储在连续内存位置
  • 链表的第一个节点可以代表整个链表
  • 查找一个节点或者访问特定编号的节点则需要O(n)的时间

定义

单链表

// 链表节点
public class ListNode {
    private int val;
    private ListNode next;

    public ListNode() {
    }

    public ListNode(int val) {
        this.val = val;
    }

    public ListNode(int val, ListNode next) {
        this.val = val;
        this.next = next;
    }
}
function listNode(val) {
  this.val = val;
  this.next = null;
}

使用场景

  1. 不确定数据结构的容量时
    • 数组大小调整的成本非常大,所以我们需要提前设置容量
    • 通常我们不知道我们需要多少空间花费
  2. 常用于遍历、检索操作较少,添加、删除操作较多的数据

链表应用实例

  1. LRU cache => 最近最少使用缓存策略
  2. Java 中 AQS 框架 => CLH 队列
  3. Java HashMap/ConcurrentHashMap => 数组 + 链表 + 红黑树
  4. Redis 中的 List
  5. C语言中的 malloc 函数实现
  6. 文件系统(FAT)

实现链表与基本操作

  • 属性
  • 构造器
  • 方法
    • 查找/获取
    • 添加
    • 更改
    • 删除
    • 获取长度
public class LinkedList {
    // Field
    private ListNode head;
    private ListNode tail;
    private int length;

    // Constructor
    public LinkedList() {
    }

    public LinkedList(ListNode head) {
        this.head = head;
        this.tail = head;
        this.length = 1;
    }

    // Method
    public int getValue(int index) {
        ListNode node = getNode(index);

        if (node == null) {
            throw new RuntimeException("The node for index is empty!");
        }

        return node.val;
    }

    public ListNode getNode(int index) {
        checkIndexRange(index);
        int currentIndex = 0;
        ListNode result = head;
        while (currentIndex < index) {
            result = result.next;
            currentIndex++;
        }
        return result;
    }

    public void add(int index, int value) {
        checkIndexRangeForAdd(index);

        if (index == length) {
            append(value);
            return;
        }
        ListNode newNode = new ListNode(value);
        if (index == 0) {
            newNode.next = head;
            this.head = newNode;
        } else {
            ListNode prevNode = getNode(index - 1);
            ListNode nextNode = prevNode.next;
            prevNode.next = newNode;
            newNode.next = nextNode;
        }
        length++;
    }

    public void addWithDummyNode(int index, int value) {
        checkIndexRangeForAdd(index);

        if (index == length) {
            append(value);
            return;
        }

        ListNode newNode = new ListNode(value);

        // Build dummy node
        ListNode dummyNode = new ListNode(-1);
        dummyNode.next = this.head;
        ListNode prevNode = dummyNode;

        while (index-- != 0) {
            prevNode = prevNode.next;
        }

        ListNode nextNode = prevNode.next;
        prevNode.next = newNode;
        newNode.next = nextNode;
        this.head = dummyNode.next;
        length++;
    }


    public void append(int value) {
        ListNode node = new ListNode(value);
        if (this.tail == null) {
            this.head = node;
        } else {
            this.tail.next = node;
        }
        this.tail = node;
        length++;
    }

    public void set(int index, int value) {
        ListNode node = getNode(index);
        node.val = value;
    }

    public int removeByIndex(int index) {
        checkIndexRange(index);
        length--;
        ListNode removeNode;
        if (index == 0) {
            removeNode = head;
            this.head = head.next;
        } else {
            ListNode prevNode = getNode(index - 1);
            if (index == length - 1) {
                removeNode = tail;
                prevNode.next = null;
                this.tail = prevNode;
            } else {
                removeNode = prevNode.next;
                prevNode.next = removeNode.next;
            }
        }
        removeNode.next = null;
        return removeNode.val;
    }

    public int removeByIndexWithDummyNode(int index) {
        checkIndexRange(index);
        length--;

        // Build dummy node
        ListNode dummyNode = new ListNode(-1);
        dummyNode.next = head;
        ListNode prevNode = dummyNode;

        while (index > 0) {
            prevNode = prevNode.next;
            index--;
        }

        ListNode removeNode = prevNode.next;
        prevNode.next = removeNode.next;

        if (index == length - 1) {
            prevNode.next = null;
            this.tail = prevNode;
        }

        removeNode.next = null;
        this.head = dummyNode.next;
        return removeNode.val;
    }

    public void removeByValue(int value) {
        ListNode prevNode = head;
        ListNode currentNode = head;
        while (currentNode != null) {
            ListNode nextNode = currentNode.next;
            if (prevNode == currentNode && currentNode.val == value) {
                // 删除头节点
                prevNode = nextNode;
                this.head.next = null;
                this.head = nextNode;
            } else {
                if (currentNode.val == value) {
                    prevNode.next = nextNode;
                    currentNode.next = null;
                } else {
                    prevNode = currentNode;
                }
            }
            currentNode = nextNode;
        }
    }

    public int getLength() {
        return length;
    }

    public int getLengthByTraverse() {
        int len = 0;
        ListNode currentNode = head;
        while (currentNode != null) {
            len++;
            currentNode = currentNode.next;
        }

        return len;
    }


    private void checkIndexRange(int index) {
        if (index < 0 || index >= length) {
            throw new RuntimeException("The index is invalid!");
        }
    }

    private void checkIndexRangeForAdd(int index) {
        if (index < 0 || index > length) {
            throw new RuntimeException("The index is invalid!");
        }
    }

    public static void main(String[] args) {
        LinkedList linkedList = new LinkedList();
        linkedList.add(0, 2);
        linkedList.add(1, 3);
        linkedList.add(2, 1);
        linkedList.add(3, 6);
        linkedList.add(4, 5);
        linkedList.add(5, 2);

        linkedList.removeByValue(2);
        System.out.println(linkedList);
    }
}

// 链表节点
class ListNode {
    int val;
    ListNode next;

    public ListNode() {
    }

    public ListNode(int val) {
        this.val = val;
    }

    public ListNode(int val, ListNode next) {
        this.val = val;
        this.next = next;
    }
}

Dummy Node 哨兵

  1. 简化边界情况 => 使得链表原头节点不再特殊
  2. 链表总是存在至少一个节点,但链表的真正元素是从哨兵节点的下一个节点开始
// Build dummy node
ListNode dummyNode = new ListNode(-1);
dummyNode.next = head;
ListNode prevNode = dummyNode;
  • 如果链表的数据结构发生变化,则需要考虑使用 Dummy Node => 如:add/remove 操作
  • 链表节点只能通过前一个节点的指针访问,在将当前节点分配给新节点之前,不要更改上一个节点的 next 指针,这样会丢失当前节点

总结

两类问题

  1. 与计数或位置相关的问题
  2. 与链表结构变化相关的问题

方法

  1. Dummy Node
  2. 链表基本操作 => 插入、删除、翻转
  3. 同向型双指针 => 快慢指针

获取index处的节点

ListNode currentNode = head;
while (index-- > 0) {
    currentNode = currentNode.next;
}
return currentNode;

ListNode currentNode = head;
for (int i = 0; i < index - 1; i++) {
    currentNode = currentNode.next
}
return currentNode;

同向型双指针(快慢指针)

public boolean fastSlowPoint(ListNode head) {
    if (head == null) {
        return false;
    }

    ListNode fast = head;
    ListNode slow = head;

    while (fast != null && fast.next != null) {
        fast = fast.next.next;
        slow = slow.next;
        // do something 
    }
}

链表问题关键步骤

  1. 快慢双指针
  2. 链表结构会不会变化 => 头节点会不会变化 => Dummy Node
  3. 翻转链表用头插法
  4. 画图关注节点连接和断开 => 画出最后的 null 节点

翻转链表

  1. 全部翻转 => prev = null
     ListNode prev = null;
     ListNode currentNode = head;
     while (head != null) {
         ListNode temp = currentNode.next;
         currentNode.next = prev;
         prev = head;
         head = temp;
     }
    
     return prev;
    
  2. 部分翻转 => prev = ListNode
    ListNode prev = xxx;
    ListNode currentNode = prev.next;
    while (currentNode != null) {
         ListNode temp = currentNode.next;
         currentNode.next = temp.next;
         temp.next = prev.next;
         prev.next = temp;
    }
    

判断链表够不够 K 个

public class Demo {
    public boolean enoughK(int k, ListNode head) {
        for (int i = 0; i < k; i++) {
            if (head == null) {
                return false;
            }
            head = head.next;
        }
        return true;
    }
}

中点问题

  • 链表长度奇数 => middle 只有一个
  • 链表长度偶数 => middle 有两个
    1. 求前一个 => fast 先走一步
    2. 求后一个 => fast slow 一起走

知识点

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

推荐阅读更多精彩内容