(四)数据结构之链表

思维导图

什么是链表:

  链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(Pointer)。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而顺序表相应的时间复杂度分别是O(logn)和O(1)。

使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。

在计算机科学中,链表作为一种基础的数据结构可以用来生成其它类型的数据结构。链表通常由一连串节点组成,每个节点包含任意的实例数据(data fields)和一或两个用来指向上一个/或下一个节点的位置的链接("links")。链表最明显的好处就是,常规数组排列关联项目的方式可能不同于这些数据项目在记忆体或磁盘上顺序,数据的访问往往要在不同的排列顺序中转换。而链表是一种自我指示数据类型,因为它包含指向另一个相同类型的数据的指针(链接)。链表允许插入和移除表上任意位置上的节点,但是不允许随机存取。链表有很多种不同的类型:单向链表,双向链表以及循环链表。

链表可以在多种编程语言中实现。像LispScheme这样的语言的内建数据类型中就包含了链表的访问和操作。程序语言或面向对象语言,如C/C++和Java依靠易变工具来生成链表。

特点:

•最简单的动态数据结构
•可以辅助组成其他数据结构
•数据存储在“节点”(Node)中
•不一定按顺序存储
•真正的动态,不需要处理固定容量的问题
•没有随机访问的能力

图解:

数组和链表的对比:

前面我们学过动态数组,栈,队列,它们底层都依托于静态数组,靠resize解决固定容量问题,而链表却是真正的动态数据结构。

•数组最好用于索引有语义的情况
•数组优点:支持快速查询
•链表不适合用于索引有语义的情况
•链表优点:动态

代码实现步骤:

节点代码实现:

我们先定义一个叫做节点的内部类:

    //节点
    class Node{
        private E data;//数据域
        private Node next;//结点指针
        //部分代码可不写,仅作理解之用
        //无参时数据域和节点域都为空
        public Node(){
            this.data = null;
            this.next = null;
        }
        //数据域为data,节点域为空
        public Node(E data){
            this.data = data;
            this.next = null;
        }

        //数据域为data,节点域为next
        public Node(E data,Node next){
            this.data = data;
            this.next = next;
        }
    }

初看代码好像挺难理解,为什么Node(结点)里面又有Node?
其实细细想想挺好理解,JAVA的对象都是引用型的,比如:
① Node node ;
② Node node1 = new Node();
③ Node node2= new Node(data,next);

①里的代码只声明了一个node引用型变量,而没有new一个Node对象,我们可以把node这个引用理解为"指针"(其实不是,为什么可以去查查资料),此时node并没有指向任何一个对象,可以理解为一个空指针(我存在,但我不指向其他对象);
②里的代码new了一个Node对象,node1这个指针就指向了这个对象,此时是空的构造方法,
data为null,next也为null,next也是Node类型的,next此时和①一样;
③ 里的代码也new了一个Node对象,node2指针指向这个对象,因为此时是有参数的构造方法,data这个数据域也就有了值(传进来的data),next这个指针此时指向传进来的next。
就和老式的火车厢一样,一个火车厢包括车厢挂钩,挂钩又可以连着下一节火车厢
火车厢 = 车厢+挂钩,而挂钩又可以连着下一个火车厢
①②③通俗化:
①知道有个火车厢,但是它即没有车厢也没有挂钩(???),即它只是一个概念,没有实体
②知道有个火车厢,有车厢也有挂钩(车厢里没东西,挂钩也没有连着下一节火车厢)
③知道有个火车厢,有车厢也有挂钩(车厢里放着东西,挂钩也连着下一节火车厢)
好,现在我们开始正式的代码:

代码部分(虚拟头结点):

public class LinkedList<E> {
    //结点
    class Node {
        private E data;//数据域
        private Node next;//结点指针

        //无参时数据域和节点域都为空
        public Node() {
            this.data = null;
            this.next = null;
        }

        //数据域为data,节点域为空
        public Node(E data) {
            this.data = data;
            this.next = null;
        }

        //数据域为data,节点域为next
        public Node(E data, Node next) {
            this.data = data;
            this.next = next;
        }
    }

    private Node dummyhead; //虚拟头结点,此结点不存储数据(dummyhead可以理解为指针)
    private int size;//链表数据个数

    public LinkedList() {
        dummyhead = new Node(null, null);
        size = 0;
    }

    //当前元素个数
    public int size() {
        return size;
    }

    //链表是否为空
    public boolean isEmpty() {
        return size == 0;
    }


    //在链表的index位置添加新数据,在链表中不是一个常用的操作
    public void add(int index, E data) {
        if (index < 0 || index > size) {
            throw new IllegalArgumentException("Index is illegal");
        }
        Node pre = dummyhead;
        for (int i = 0; i < index; i++) {
            pre = pre.next;
        }
        pre.next = new Node(data, pre.next);
        size++;
    }

    //在链表首(虚拟头结点后)位置添加数据
    public void addFirst(E data) {
        add(0, data);
    }

    //在链表尾添加数据
    public void addLast(E data) {
        add(size, data);
    }

    //获取链表指定索引处的数据
    public E get(int index) {
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("Index is illegal");
        }
        Node current = dummyhead.next;
        for (int i = 0; i < index; i++) {
            current = current.next;
        }
        return current.data;
    }

    //获取链表0索引处的数据
    public E getFirst() {
        return get(0);
    }

    //获取链表表尾处的数据
    public E getLast() {
        return get(size - 1);
    }

    //修改链表指定索引处的数据
    public void set(int index, E data) {
        if (index < 0 || index > size) {
            throw new IllegalArgumentException("Index is illegal");
        }
        Node current = dummyhead.next;
        for (int i = 0; i < index; i++) {
            current = current.next;
        }
        current.data = data;
    }

    //查看链表是否包含某数据
    public boolean contains(E data) {
        Node current = dummyhead.next;
        while (current != null) {
            if (current.data == data) {
                return true;
            }
            current = current.next;
        }
        return false;
    }

    //删除链表指定索引的数据
    public E remove(int index) {
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("Index is illegal");
        }
        Node pre = dummyhead;
        for (int i = 0; i < index; i++) {
            pre = pre.next;
        }
        Node ret = pre.next;
        pre.next = ret.next;
        ret.next = null;
        size--;
        return ret.data;
    }

    //删除0索引的数据
    public E removeFirst() {
        return remove(0);
    }

    //删除最后一个数据
    public E removeLast() {
        return remove(size - 1);
    }

    @Override
    public String toString() {
        StringBuilder res = new StringBuilder();
        res.append("LinkedList:");
        for (Node current = dummyhead.next; current != null; current = current.next) {
            res.append(current.data + "-->");
        }
        res.append("NULL");
        return res.toString();
    }
}
这里的代码用到了虚拟头结点,什么是虚拟头结点,不用它有什么坏处?

我们先说不用虚拟头结点的坏处,如果我们想在链表头添加一个结点,我们需要这么写:

//结点代码与上文相同,省略
private Node head;
    private int size;

    public LinkedList() {
        head = null;
        size = 0;
    }
//      在表头插入数据
       public void addFirst(E data) {
        head = new Node(data, head);
//       上一行代码相当于下面代码的缩写
//        Node prev = new Node(data);
//        prev.next = head;
//        head = prev;
        size++;
    }

    public void add(int index, E data) {
        if (index < 0 || index > size) {
            throw new IllegalArgumentException("Index is illegal");
        }
        if (index == 0) {
            addFirst(data);
        } else {
            Node prev = head;
            for (int i = 0; i < index - 1; i++) {
                prev = prev.next;
            }
            //缩写和前一个方法类似
            prev.next = new Node(data, prev.next);
            size++;
        }
    }
    }
//后续代码省略

在表头插入数据需要这样:

第一步

第二步

在其他结点插入数据需要这样:
第一步

第二步

都是插入操作其代码却不能统一,在头结点插入数据能不能和其他结点插入操作一样呢,这时候我们就需要虚拟头结点了。

虚拟头结点的概念:

虚拟头结点:链表的增加和删除操作,一定要通过待处理节点的前一个节点来实现,为了解决头结点没有前继节点的特殊性,需要给链表添加一个虚拟头结点。新建指针一个用于索引的指针dummyHead, 指向待处理节点的前继节点,则待处理节点为dummyHead.next. 初始化dummyHead则指向链表的虚拟头结点。
注意:虚拟头结点不存储任何数据,和循环队列差不多
其中,虚拟头结点的图示:


添加操作
先新建一个结点指针prev,令其指向dummyHead结点,prev指针经循环操作后到要添加的结点的前一个位置,然后进行如下操作:

删除操作
先新建一个结点指针prev,令其指向dummyHead结点,prev指针经循环操作后到要删除的结点的前一个位置,然后进行如下操作:


为什么在链表表尾添加数据和删除数据传入的参数不同?
  因为添加和删除操作均要操作待处理结点的前一个结点来实现,在表尾添加数据时依托于链表中存在的最后一个结点(将其作为前继结点),而表尾删除数据时删除的是最后一个结点,此时依托倒数于第二个结点(将其作为前继结点)。

测试结果:

时间复杂度:

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

推荐阅读更多精彩内容