链表基础知识

概述

        前面我们讲的线性表的顺序存储结构,它是有缺点的, 最大的缺点就是插入和删 除时需要移动大量元素,这显然就需要耗费时间,那我们能不能想办法解决呢?
        要解决这个问题,我们就得考虑一下导 致这个问题的原因。
        为什么当插入和删除时,就要移动大量元素,仔细分析后,发现原因就在于相邻 两元素的存储位置也具有邻居关系。它们编号是1,2,3,...,n 它们在内存中的位 置也是挨着的,中间没有空隙当然就无法快速介入,而 删除后,当中就会留 出空隙,自然需要弥补 。问 题就出在这 里。
        A同学思路:让当中每个元素之间都留有一个空位置,这样要插入时,就不至于移动。可一个空位置如何解决多个相同位置插入数据的问题呢?所以这个想法显然不 行。
        B 同学思路: 那就让当中每个元素之间都留足够多的位置,根据实际情况制定空 隙大小,比如 10 个,这样插入时,就不需要移动了。万一 10 个空位用完了,再考虑 移动使得每个位置之间都有 10 空位置。 如果删除,就直接删掉,把位置留空即 可。这样似乎暂时解快了插入和删除的移动数据问题。可这对于超过 同位置数据的插入,效率上还是存在问题。对于数据的遍历,也会因为空位置大多而造成判断 时间上的液费。而且显然这里空间复杂度还增加了,因为每个元素之间都有若干个空 位置。
        C同学思路: 我们反正也是要让相邻元素间留有足够余地,那干脆所有的元素都不要考虑相邻位置了,哪有空位就到哪里,而只是让每个元素知道它下个元素的位置在哪里,这样,我们可以在第一个元素时,就知道第二个元素的位置(内存地址) , 而找到它;在第二个元素时,再找到第三个元素的位置(内存地址)。这样所有的元素我们就都可以通过遍历而找到。
        很明显C同学这个想法非常好!我们要的就是这个思路。 因此: java 集合中又引入了一个 LinkedList ,即链表结构。

1 链表

1.1 概念

        那么什么是链表呢?链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的。

        为了表示 数据ai与其直 接后继数据元素 ai+1之 间的逻辑关系,对数据元素ai来说, 除了存储 其本 身的信息之外,还需存储一 个指示 直接后继 的信息(即直接后继的存储位置 )。我们 把存储数据 元素信息 的域称为 数据域 把存储直接后继位置的域称为 指针域 。指针域中存储的信息称做指针或链。这两部分信息组 成数据元素ai的存储映像,称为 (No de)。

        对于线性表来说,总得有个头有个尾,链表也不例外。我们把链表中 第一个结点的存储位置叫做 头指针 ,那么整个链表的存取就必须是从头指针开始进行了。之后的每一个节点,其实就是上一个后继指针指向的位置想象一下,最后一个节点,它的指针指向哪里呢?
        最后一个, 当然就意味着直接后继不存在了,所以我们规定,线性链表的最后一个结点指针为"空"(通常用 NULL或"^"符号表示 )。

1.从上图可以看出,链式结构在逻辑上是连续的,但是在物理上不一定连续。

2.现实中的结点一般都是从堆上申请出来的。

3.从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续。

        有时,我们为了更加方便地对链表进行操作,会在单链表的第一个结点前附设一个结点,称为头结点。头结点的数据域可以不存储任何信息,谁叫它是第一个呢,有这个特权。也可以存储如线性表的长度等附加信息,头结点的指针域存储指向第一个节点的指针。

1.2 头结点与头指针的异同

头指针

  • 1.头指针是链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针。
  • 2.头指针具有标识作用,所以常用头指针冠以链表的名字。
  • 3.无论链表是否为空,头指针均不为空。头指针是链表的必要元素。

头结点

  • 1.头结点是为了操作的统一和方便而设立的。放在第一元素的结点之前,其数据域一般无异议(也可以存放链表的长度)。
  • 2.有了头结点,对在第一元素结点前插入结点和删除第一结点,其操作与其他结点的操作就统一了。
  • 3.头结点不一定是链表必须要素。

实际中链表的结构非常多样:

1.单向或者双向

2.带头或者不带头

3.循环或者非循环

2 链表的实现

2.1 节点的创建

       链表是由一个个节点所组成的,后一个结点依靠前一个才能找到,而节点是由数据域(value),以及指针域(next)组成,对于数据域,其是引用类型,存放下一个节点的地址,同时设置构造函数对val进行初始化。

public class MySingleLinkedList {
    class ListNode{
        public int val;
        public ListNode next;

        public ListNode(int val, ListNode next) {
            this.val = val;
            next = null;
        }
    }
    public ListNode head;//代表链表的头结点
}

2.2 创建链表 

创建完节点后,我们就要来创建一个链表

public void createList() {
        ListNode node1 = new ListNode(12);
        ListNode node2 = new ListNode(23);
        ListNode node3 = new ListNode(34);
        ListNode node4 = new ListNode(45);
        ListNode node5 = new ListNode(56);

        node1.next = node2;
        node2.next = node3;
        node3.next = node4;
        node4.next = node5;

        this.head = node1;
    }

 2.3 链表的操作

对于一个创建好节点的链表,它的操作有以下这些:

//头插法
public void addFirst(int data){
}
//尾插法
public void addLast(int data){
}
//任意位置插入,第一个数据节点为0号下标
public void addIndex(int index,int data){
}
//查找是否包含关键字key是否在单链表当中
public boolean contains(int key){
return false;
}
//删除第一次出现关键字为key的节点
public void remove(int key){
}
//删除所有值为key的节点
public void removeAllKey(int key){
}
//得到单链表的长度
public int size(){
return -1;
}
public void clear() {
}
public void display() {}

2.4 链表的插入

链表的插入有三种,头插法尾插法指定位置插入

1.头插法

        链表的头插法(Head Insertion)是指在链表的头部添加新节点的操作。在单链表中,这种操作相对简单,因为不需要改变已有节点的指针,只需要创建一个新的节点,并将其链接到当前链表的头部。

  1. 创建新节点:为新节点分配内存,并设置它的数据域(通常是新插入的数据)和前驱指针(指向当前链表的头节点)。

  2. 更新头指针:将新节点的后继指针设置为原来的头节点,这样新节点就成为了新的链表头。

  3. 如果原链表为空(即头指针是 或者指向空),则新节点同时是头节点和尾节点。

	//头插法
    public void addFirst(int val) {
        Node node = new Node(val);
        //判断链表是否为空
        if(size == 0){
            this.head = node;
        }else {
            //链表不为空
            node.next = head;
            head = node;
        }
        //元素个数+1
        this.size++;
    }

 代码语句的顺序不能变,即

node.next = head.next

head.next = node

如果编写顺序调换,head.next的原始数据会丢失。

2.尾差法

        链表的尾插法是一种在链表的末尾添加新节点的方法,它通常用于动态数据结构中,因为不需要像数组那样预先知道链表的长度。

  1. 创建新节点:首先,你需要创建一个新的节点,包含你要插入的数据。

  2. 获取尾指针:找到当前链表的最后一个节点。如果链表为空,新节点就是第一个节点(头节点)。

  3. 更新指针:将新节点的指针指向当前的尾节点,表示新节点是下一个元素。next

  4. 设置尾节点:如果当前节点就是链表的最后一个节点,那么它的指针也需要更新为新节点,这样新节点就成为了新的尾节点。

public void addLast(int val) {
        ListNode node = new ListNode(val);
        if(head == null) {
            head = node;
            return;
        }
        ListNode cur = head;
        while (cur.next != null) {
            cur = cur.next;
        }
        cur.next = node;
    }
 3.任意位置插入

        现在,我们要在index位置插入一个整形的value,我们要如何操作呢?既然是在index的位置插入,那么我们就需要知道这个位置前一个位置的信息,所以我们要把cur走index-1步,即在第3位置插入我们的cur就要走2步。

public void addIndex(int index,int val) {
        //1.判断index的合法性
        try {
            checkIndex(index);
        }catch (IndexNotLegalException e) {
            e.printStackTrace();
        }
        //2.index == 0  || index == size()
        if(index == 0) {
            addFirst(val);
            return;
        }
        if(index == size()) {
            addLast(val);
            return;
        }
        //3. 找到index的前一个位置
        ListNode cur = findIndexSubOne(index);
        //4. 进行连接
        ListNode node = new ListNode(val);
        node.next = cur.next;
        cur.next = node;
    }
    private ListNode findIndexSubOne(int index) {
        int count = 0;
        ListNode cur = head;
        while (count != index-1) {
            cur = cur.next;
            count++;
        }
        return cur;
    }

    private void checkIndex(int index) throws IndexNotLegalException{
        if(index < 0 || index > size()) {
            throw new IndexNotLegalException("index不合法");
        }
    }

 2.5 查找是否包含关键字key是否在单链表当中

 怎么查找呢?我们只要对链表进行遍历即可

public boolean contains(int val) {
        ListNode cur = head;
        while (cur != null) {
            if(cur.val == val) {
                return true;
            }
            cur = cur.next;
        }
        return false;
    }

 2.6 链表的删除

1 删除第一次出现关键字为key的节点  

现在我们有这么一个链表

我们要删除其中的34这个值,我们应该如何操作?

这个问题的关键所在就是找到34的前一个value,并且删除当前34位置的值。

 public void remove(int val) {
        //判断是否为空
        if(head == null) {
            return;
        }
        if(head.val == val) {
            head = head.next;
            return;
        }
        ListNode cur = head;
        while (cur.next != null) {
            if(cur.next.val == val) {
                ListNode del = cur.next;
                cur.next = del.next;
                return;
            }
            cur = cur.next;
        }
    }
删除所有值为key的节点

 

         与删除一个元素不同的是,删除所有值为key的节点,在循环判断时找到指定元素时不退出,继续进行查找,直到链表遍历完成.

public void removeAllKey(int val) {
        //1. 判空
        if(this.head == null) {
            return;
        }
        //2. 定义prev 和 cur
        ListNode prev = head;
        ListNode cur = head.next;
        //3.开始判断并且删除
        while(cur != null) {
            if(cur.val == val) {
                prev.next = cur.next;
                //cur = cur.next;
            }else {
                prev = cur;//prev = prev.next;
                //cur = cur.next;
            }
            cur = cur.next;
        }
        //4.处理头节点
        if(head.val == val) {
            head = head.next;
        }
    }
3 清空链表 

要想清空一个链表,我们直接head = null其实就行了,不过我们也可以对每个节点都置空。

public void clear() {
        //head = null;
        ListNode cur = head;
        while (cur != null) {
            ListNode curN = cur.next;
            //cur.val = null;
            cur.next = null;
            cur = curN;
        }
        head = null;
    }

 2.7 链表的翻转

 public ListNode reverseList() {
        if(head == null) {
            return head;
        }
        ListNode cur = head.next;
        head.next = null;

        while(cur != null) {
            ListNode curN = cur.next;
            //开始翻转
            cur.next = head;
            head = cur;
            cur = curN;
        }
        return head;
    }

对于链表而言,上述代码均为对无头单向非循环链表实现,而我们的链表还有很多,比如无头双向链表实现

2.8 无头双向链表的实现 

public class MyLinkedList {
    static class ListNode {
        public int val;
        public ListNode prev;//前驱
        public ListNode next;//后继

        public ListNode(int val) {
            this.val = val;
        }
    }
    public ListNode head;//标志头节点
    public ListNode last;//标志尾结点

    //得到双向链表的长度
    public int size(){
        int count = 0;
        ListNode cur = head;
        while (cur != null) {
            count++;
            cur = cur.next;
        }
        return count;
    }

    public void display(){
        ListNode cur = head;
        while (cur != null) {
            System.out.print(cur.val+" ");
            cur = cur.next;
        }
        System.out.println();
    }

    //查找是否包含关键字key是否在单链表当中
    public boolean contains(int key){
        ListNode cur = head;
        while (cur != null) {
            if(cur.val == key) {
                return true;
            }
            cur = cur.next;
        }
        return false;
    }


    //头插法
    public void addFirst(int data){
        ListNode node = new ListNode(data);
        if(head == null) {
            //是不是第一次插入节点
            head = last = node;
        }else {
            node.next = head;
            head.prev = node;
            head = node;
        }
    }

    //尾插法
    public void addLast(int data){
        ListNode node = new ListNode(data);
        if(head == null) {
            //是不是第一次插入节点
            head = last = node;
        }else {
            last.next = node;
            node.prev = last;
            last = last.next;
        }
    }
    //任意位置插入,第一个数据节点为0号下标
    public void addIndex(int index,int data){
        try {
            checkIndex(index);
        }catch (IndexNotLegalException e) {
            e.printStackTrace();
        }
        if(index == 0) {
            addFirst(data);
            return;
        }
        if(index == size()) {
            addLast(data);
            return;
        }
        //1. 找到index位置
        ListNode cur = findIndex(index);
        ListNode node = new ListNode(data);
        //2、开始绑定节点
        node.next = cur;
        cur.prev.next = node;
        node.prev = cur.prev;
        cur.prev = node;
    }

    private ListNode findIndex(int index) {
        ListNode cur = head;
        while (index != 0) {
            cur = cur.next;
            index--;
        }
        return cur;
    }
    private void checkIndex(int index) {
        if(index < 0 || index > size()) {
            throw new IndexNotLegalException("双向链表插入index位置不合法: "+index);
        }
    }

    //删除第一次出现关键字为key的节点
    public void remove(int key){
        ListNode cur = head;
        while (cur != null) {
            if(cur.val == key) {
                //开始删除 处理头节点
                if(cur == head) {
                    head = head.next;
                    if(head != null) {
                        head.prev = null;
                    }else {
                        //head == null 证明只有1个节点
                        last = null;
                    }
                }else {
                    cur.prev.next = cur.next;
                    if(cur.next == null) {
                        //处理尾巴节点
                        last = last.prev;
                    }else {
                        cur.next.prev = cur.prev;
                    }
                }
                return;//删完一个就走
            }
            cur = cur.next;
        }
    }
    //删除所有值为key的节点
    public void removeAllKey(int key){
        ListNode cur = head;
        while (cur != null) {
            if(cur.val == key) {
                //开始删除 处理头节点
                if(cur == head) {
                    head = head.next;
                    if(head != null) {
                        head.prev = null;
                    }else {
                        //head == null 证明只有1个节点
                        last = null;
                    }
                }else {
                    cur.prev.next = cur.next;
                    if(cur.next == null) {
                        //处理尾巴节点
                        last = last.prev;
                    }else {
                        cur.next.prev = cur.prev;
                    }
                }
                //return;//删完一个就走
            }
            cur = cur.next;
        }
    }
    //清空链表
    public void clear(){
        ListNode cur = head;
        while (cur != null) {
            ListNode curN = cur.next;
            //cur.val = null;
            cur.prev = null;
            cur.next = null;
            cur = curN;
        }
        head = last = null;
    }
}

 3 LinkedList

3.1 什么是LinkedList

        LinkedList 的底层是 双向链表结构 ,由于链表没有将元素存储在连续的空间中,元素存储在单独的节点中,然后通过引用将节点连接起来了,因此在在任意位置插入或者删除元素时,不需要搬移元素,效率比较高。

 

在集合框架中,LinkedList也实现了List接口:

  1. LinkedList实现了List接口
  2. LinkedList的底层使用了双向链表
  3. LinkedList没有实现RandomAccess接口,因此LinkedList不支持随机访问
  4. LinkedList的任意位置插入和删除元素时效率比较高,时间复杂度为O(1)
  5. LinkedList比较适合任意位置插入的场景 

 3.2 LinkedList的使用

 

public static void main(String[] args) {
        //无参构造
        List<Integer> list1 = new LinkedList<>();
        //有参构造
        List<String> list2 = new ArrayList<>();
        list2.add("JavaSE");
        list2.add("JavaWeb");
        list2.add("JavaEE");
        // 使用ArrayList构造LinkedList
        List<String> list3 = new LinkedList<>(list2);
    }

 4 ArrayList和LinkedList的区别

 

  • 16
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值