数据结构:链表(链式存储的线性表)

1、定义

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针连接次序实现的。

2、组成

链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。

2.1、数据域

存储数据元素。

2.2、指针域

存储下一个结点的地址。

3、类型

3.1、单向链表

链表中的每个结点只有指向下一个结点的指针域。


image.png

3.2、双向链表

链表中的每个结点既有指向上一个结点的指针域,又有指向下一个结点的指针域。


image.png

3.3、循环链表

单向链表中尾结点的指针指向头结点。


image.png

4、特点

4.1、优点

在链表头尾新增与删除结点的时间复杂度是O(1);适用于存储元素个数不确定的数据。需要多少内存占用多少内存,内存利用率高。

4.2、缺点

在链表头尾之间新增与删除结点往往需要先查找增删的具体位置,而链表只能从头到尾访问结点元素,时间复杂度是O(n)。

5、Java代码实现链表及相关习题

package com.zy.demo.util;

import java.util.Arrays;

/**
 * 链表
 * @author zy
 */
public class LinkedListUtil<E> {

    //链表头结点
    private Node<E> first;

    //链表尾结点
    private Node<E> last;

    //链表中结点个数
    private int size;

    /**
     * 链表的无参构造函数
     */
    public LinkedListUtil(){

    }

    /**
     * 链表初始化头结点的构造函数
     * @param e 元素
     */
    public LinkedListUtil(E e){
        //链表中只有1个结点,头尾结点相同
        this.first = this.last = new Node<>(e,null);
        size++;
    }

    /**
     * 获取链表长度
     * @return 链表中结点的个数
     */
    public int size(){
        return this.size;
    }

    /**
     * 按照从头到尾的顺序打印链表元素
     * @return 返回结果用"{}"包裹,多个元素之间","分隔
     */
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        Node<E> node = this.first;
        while (node != null){
            sb.append(node.e).append(",");
            node = node.next;
        }
        return sb.length() > 0 ? "{"+sb.deleteCharAt(sb.length()-1).toString()+"}" : "{}";
    }

    /**
     * 判断链表中指定索引处的结点是否存在。规定:链表索引取值从0到链表长度减1。
     * 复杂度:O(1)
     * @param index 索引
     * @return 存在则返回true;否则返回false。
     */
    public boolean existIndex(int index){
        return this.size > 0 && index >= 0 && index < this.size;
    }

    /**
     * 判断链表中指定元素的结点是否存在
     * 时间复杂度:O(n) --遍历1次链表查找指定元素。
     * 空间复杂度:O(n) --新建StringBuilder对象存储结果,结果与输入数据量线性相关。
     * @param e 元素
     * @return 存在则返回true;否则返回false。
     */
    public boolean existElement(E e){
        return this.size > 0 && getIndex(e) != null;
    }

    /**
     * 获取链表中指定索引处的元素
     * 时间复杂度:O(n) --遍历1次链表。
     * 空间复杂度:O(1) --使用有限的变量资源。
     * @param index 索引
     * @return 元素
     */
    public E getElement(int index){
        if(!existIndex(index)){
            return null;
        }
        //计数器
        int count = 0;
        Node<E> node = this.first;
        while (node != null){
            if(count == index){
                return node.e;
            }
            node = node.next;
            count++;
        }
        return null;
    }

    /**
     * 获取链表中指定元素所存在的索引列表
     * 时间复杂度:O(n) --遍历1次链表。
     * 空间复杂度:O(n) --新建StringBuilder对象存储结果,结果与输入数据量线性相关。
     * @param e 元素
     * @return 索引列表
     */
    public int[] getIndex(E e){
        //索引
        int index = 0;
        //临时变量用于收集未知个数的索引值
        StringBuilder sb = new StringBuilder();
        //获取头结点
        Node<E> node = this.first;
        //头结点不为空时循环获取下个结点
        while (node != null){
            //在链表中找到指定元素时,记录索引值。
            if(node.e == e){
                sb.append(index).append(",");
            }
            node = node.next;
            index++;
        }
        //String[]转换int[]
        if(sb.length() > 0){
            String indexStr = sb.deleteCharAt(sb.length()-1).toString();
            return Arrays.stream(indexStr.split(",")).mapToInt(Integer::parseInt).toArray();
        }
        return null;
    }

    /**
     * 在指定索引处添加指定结点
     * 时间复杂度:O(n) --遍历1次链表。
     * 空间复杂度:O(1) --使用有限的变量资源。
     * @param index 索引
     * @param e 元素
     * @return 添加成功则返回true;否则返回false。
     */
    public boolean add(int index,E e){
        //如果是空链表,允许在第一个位置添加结点
        if(this.size == 0 && index == 0){
            this.first = this.last = new Node<>(e,null);
            this.size++;
            return true;
        }
        //校验输入的索引
        if(!existIndex(index)){
            return false;
        }
        //计数器
        int count = 0;
        //缓存上一个结点
        Node<E> prevNode = null;
        //从头结点开始遍历
        Node<E> node = this.first;
        //这里头结点其实肯定不为null
        while (node != null){
            //按照计数器查找指定索引处的结点
            if(count == index){
                //在链表头结点插入时,新插入的结点为链表的头结点,并指向原索引处的结点。。
                if(index == 0){
                    //头结点更改
                    this.first = new Node<>(e,node);
                    this.size++;
                    return true;
                }else{
                    //在链表非头结点插入时,上个结点要指向新插入的结点,新插入的结点要指向原索引处的结点。
                    prevNode.next = new Node<>(e, node);
                    this.size++;
                    return true;
                }
            }
            prevNode = node;
            node = node.next;
            count++;
        }
        return false;
    }

    /**
     * 在链表尾添加元素
     * 时间复杂度:O(1) --只需修改一次链表尾结点的指针。
     * 空间复杂度:O(1) --使用有限的变量资源。
     * @param e 元素
     * @return 添加成功则返回true;否则返回false。
     */
    public boolean addLast(E e){
        //如果链表是空的,则新插入的结点是头结点也是尾结点。
        if(this.size == 0){
            this.first = this.last = new Node<>(e,null);
        }else{
            //取出链表尾结点
            Node<E> node = this.last;
            //原尾结点指针指向新添加的结点,新添加的结点作为链表的新尾结点。
            this.last = node.next = new Node<>(e,null);
        }
        this.size++;
        return true;
    }

    /**
     * 修改链表中指定索引处的元素
     * 时间复杂度:O(n) --遍历1次链表查找指定元素。
     * 空间复杂度:O(1) --使用有限的变量资源。
     * @param index 索引
     * @param e 元素
     * @return 修改成功则返回true;否则返回false。
     */
    public boolean mod(int index,E e){
        //判断链表中是否存在索引对应的结点
        if(!existIndex(index)){
            return false;
        }
        //计数器
        int count = 0;
        //获取链表头结点
        Node<E> node = this.first;
        //从头遍历链表
        while (node != null){
            if(index == count){
                node.e = e;
                return true;
            }
            node = node.next;
            count++;
        }
        return false;
    }

    /**
     * 替换链表中的指定元素
     * 时间复杂度:O(n) --遍历1次链表查找指定元素。
     * 空间复杂度:O(1) --使用有限的变量资源。
     * @param oldElement 链表中原元素
     * @param newElement 链表中新元素
     * @return 替换成功则返回true;否则返回false。
     */
    public boolean replace(E oldElement,E newElement){
        if(this.size == 0){
            return false;
        }
        //替换操作计数器
        int count = 0;
        Node<E> node = this.first;
        while(node != null){
            if(node.e == oldElement){
                node.e = newElement;
                count++;
            }
            node = node.next;
        }
        return count > 0;
    }

    /**
     * 删除指定索引处的元素
     * 时间复杂度:O(n) --遍历1次链表查找指定元素。
     * 空间复杂度:O(1) --使用有限的变量资源。
     * @param index 索引
     * @return 删除成功则返回true;否则返回false。
     */
    public boolean remove(int index){
        //校验索引规则
        if(!existIndex(index)){
            return false;
        }
        //索引计数器
        int count = 0;
        //缓存上一个结点
        Node<E> prevNode = null;
        //获取头结点
        Node<E> node = this.first;
        //从头开始遍历链表
        while(node != null){
            //索引为头结点时
            if(index == 0){
                //如果头结点存在下一个结点,则更换头结点
                if(node.next != null){
                    this.first = node.next;
                }else{
                    //链表只有1个结点
                    this.first = this.last = null;
                }
                size--;
                return true;
            }else{
                //索引不为头结点时,匹配链表的索引
                if(count == index){
                    //上个结点指向删除结点的下个结点
                    prevNode.next = node.next;
                    size--;
                    return true;
                }
            }
            prevNode = node;
            node = node.next;
            count++;
        }
        return false;
    }

    /**
     * 删除链表中指定元素对应的结点
     * 时间复杂度:O(n) --遍历1次链表。
     * 空间复杂度:O(1) --使用有限的变量。
     * @param e 指定元素
     * @return 删除结点的个数
     */
    public int removeAll(E e){
        if(this.size == 0){
            return 0;
        }
        //删除计数器
        int count = 0;
        //缓存上一个结点
        Node<E> prevNode = null;
        //从头开始遍历链表
        Node<E> node = this.first;
        while(node != null){
            if(node.e == e){
                //链表只有一个结点
                if(this.size == 1){
                    this.first = this.last = null;
                }else{
                    //删除头结点
                    if(node == this.first){
                        this.first = node.next;
                    }else if(node == this.last){
                        //删除尾结点
                        prevNode.next = null;
                        this.last = prevNode;
                    }else{
                        //删除中间结点,注意保留上个结点的缓存变量
                        prevNode.next = node.next;
                        node = prevNode;
                    }
                }
                size--;
                count++;
            }
            prevNode = node;
            node = node.next;
        }
        return count;
    }

    /**
     * 翻转指定链表
     * 时间复杂度:O(2n)=O(n) --顺序执行遍历2次链表;链表添加结点时间复杂度是O(1),不计算在复杂度内。
     * 空间复杂度:O(n) --新建单链表对象。
     * @param linkList 指定链表
     * @return 翻转后的链表
     */
    public LinkedListUtil<E> reverse(LinkedListUtil<E> linkList){
        //校验指定链表
        if(linkList == null || linkList.size() == 0){
            return null;
        }
        //获取指定链表头结点
        Node<E> node = linkList.first;
        //缓存上一个结点
        Node<E> prev = null;
        //从头遍历链表,利用双链表实现翻转
        while(node != null){
            node.prev = prev;
            prev = node;
            node = node.next;
        }
        //新建单链表对象
        LinkedListUtil<E> newLinkedListUtil = new LinkedListUtil<>();
        //获取指定链表尾结点,从尾到头向新的单链表添加结点
        node = linkList.last;
        while(node != null){
            newLinkedListUtil.addLast(node.e);
            node = node.prev;
        }
        return newLinkedListUtil;
    }

    /**
     * 翻转指定链表,且指定参数k个结点翻转一次(k能被链表长度整除)。
     * 时间复杂度:O(n) --虽然嵌套循环遍历2次链表,但是内层循环次数为有限值k,并且链表在尾结点后添加元素的时间复杂度是O(1),不计算在复杂度内。
     * 空间复杂度:O(n) --新建单链表对象。
     * @param linkList 指定链表
     * @param k 翻转一次链表需要的结点数,正整数。
     * @return 翻转后的链表
     */
    public LinkedListUtil<E> reverse(LinkedListUtil<E> linkList,int k){
        //校验指定链表
        if(linkList == null || linkList.size() == 0){
            return null;
        }
        //翻转结点个数小于1不处理
        if(k < 1){
            return null;
        }
        //判断翻转结点个数是否能被链表长度整除
        if(linkList.size() % k > 0){
            return null;
        }
        //计数器
        int count = 0;
        //新建单链表对象
        LinkedListUtil<E> newLinkedListUtil = new LinkedListUtil<>();
        //获取指定链表头结点
        Node<E> node = linkList.first;
        //缓存上一个结点
        Node<E> prev = null;
        //从头遍历链表,利用双链表实现翻转
        while(node != null){
            //考虑第一次循环是头结点不为空的情况,计数器先加1
            count++;
            node.prev = prev;
            //如果循环到指定结点数时,从尾到头翻转子链表
            if(count % k == 0){
                //子链表计数器,用于每次只向新链表添加k个结点。
                int tmp = 0;
                //缓存当前node,避免影响第一层while循环的条件
                Node<E> nodeTmp = node;
                while(nodeTmp != null && tmp != k){
                    newLinkedListUtil.addLast(nodeTmp.e);
                    nodeTmp = nodeTmp.prev;
                    tmp++;
                }
            }
            prev = node;
            node = node.next;
        }
        return newLinkedListUtil;
    }

    /**
     * 如果链表长度为奇数,利用快慢指针获取链表中间结点的元素。
     * 时间复杂度:O(n/2)=O(n) --遍历1/2次链表
     * 空间复杂度:O(1) --使用有限的变量资源。
     * @param linkList 指定链表
     * @return 元素
     */
    public E getMiddleElement(LinkedListUtil<E> linkList){
        if(linkList == null){
            return null;
        }
        //判断链表长度是否为奇数
        if(linkList.size() % 2 == 0){
            return null;
        }
        //慢指针结点
        Node<E> slowNode;
        //快慢指针初始化为链表头结点
        Node<E> fastNode = slowNode = linkList.first;
        //从头开始按照快指针遍历链表,当快指针的下个结点为空时,证明链表遍历完成。
        while(fastNode != null && fastNode.next != null){
            //慢指针一次跳转1个结点
            slowNode = slowNode.next;
            //块指针一次跳转2个结点
            fastNode = fastNode.next.next;

        }
        //慢指针即中间结点
        return slowNode.e;
    }

    /**
     * 判断指定链表是否有环
     * 时间复杂度:O(n) --遍历1次链表。
     * 空间复杂度:O(1) --使用有限的变量资源。
     * @param linkList 指定链表
     * @return 如果有环则返回true;否则返回false。
     */
    public boolean existLoop(LinkedListUtil<E> linkList){
        if(linkList == null){
            return false;
        }
        if(linkList.size() == 0){
            return false;
        }
        //快慢指针初始结点
        Node<E> fastNode;
        //慢指针结点
        Node<E> slowNode;
        //快慢指针初始结点
        fastNode = slowNode = linkList.first;
        //以慢指针遍历链表
        while(slowNode != null){
            //如果快指针的下个结点为空,则说明链表无环。
            if(fastNode.next == null){
                break;
            }
            //不考虑头结点相遇的情况
            fastNode = fastNode.next.next;
            slowNode = slowNode.next;
            //如果快指针与慢指针相遇,则说明链表中有环。
            if(fastNode == slowNode){
                return true;
            }
        }
        return false;
    }

    /**
     * 结点内部类
     * @param <E> 元素
     */
    private static class Node<E>{
        //数据域(元素)
        E e;
        //上个结点
        Node<E> prev;
        //下个结点
        Node<E> next;

        //单向链表结点构造函数
        Node(E e,Node<E> next){
            this.e = e;
            this.next = next;
        }

        //双向链表结点构造函数
        Node(Node<E> prev,E e,Node<E> next){
            this.prev = prev;
            this.e = e;
            this.next = next;
        }
    }
}

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