【数据结构与算法(三)】链表

链表(Linked List)
  • 数据存储在”结点“(Node)中

在这里插入图片描述

  • 优点:不用像动态数组/栈/队列那样依托于数组,依靠resize()来“动态”管理容量。真正的动态,不需要处理固定容量的问题。
  • 缺点:丧失了随机访问的能力。
普通链表

主体

public class LinkedList<E> {
    /**
     * 定义私有内部类,外界不能直接访问。因为用户不需要清楚具体实现,
     * 对于用户来说没有必要知道链表中还有Node结点这个东西,用户只要
     * 知道怎么调用增删改查等方法就行。
     */
    private class Node{
        public E e;
        //指向下一个结点的引用
        public Node next;

        //定义三种构造函数,应对三种不同的情况
        public Node(E e,Node next){
            this.e=e;
            this.next=next;
        }
        public Node(E e){
            this(e,null);
        }
        public Node(){
            this(null,null);
        }
        @Override
        public String toString(){
            return e.toString();
        }
    }
    //指向链表头部结点
    private Node head;
    private int size;

    public LinkedList(){
        head=null;
        size=0;
    }
}

add函数

在这里插入图片描述

    //在链表头部添加结点
    public void addFirst(E e){
        head=new Node(e,head);
        size++;
    }

    /**
     * 在链表的index(0-based)位置(索引为index结点的前面)添加新的元素e
     * 在链表中不是一个常用的操作,练习用
     * 插入有两个步骤:
     * 1、找到待插入位置前一个结点,并指向它(因为0前面没有结点,所以必须做特殊处理)
     * 2、交换指向(把新结点插入进去)
     */
    public void add(int index,E e){
        if(index<0||index>size){
            throw new IllegalArgumentException("add failed,illegal index");
        }
        if(index==0)
            addFirst(e);
        else{
            Node prev=head;
            for(int i=0;i<index-1;i++){
                prev=prev.next;
            }
            /*Node node=new Node(e);
            //下面两句位置不能交换
            node.next=prev.next;
            prev.next=node;*/
            prev.next=new Node(e,prev.next);
            size++;
        }
    }

    //在链表末尾添加元素
    public void addLast(E e){
        add(size,e);
    }

其他方法

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

    //返回结点的个数
    public int getSize(){
        return size;
    }
带有虚拟头结点的链表
  • 在普通链表中添加一个指定位置的元素时,因为头结点前面没有元素,必须另做考虑。所以引申出在链表最前面添加一个虚拟头结点来解决这个问题,使逻辑统一。
    在这里插入图片描述

主体

public class DummyHeadLinkedList<E> {

    private class Node{
        public E e;
        public Node next;

        //定义三种构造函数,应对三种不同的情况
        public Node(E e,Node next){
            this.e=e;
            this.next=next;
        }
        public Node(E e){
            this(e,null);
        }
        public Node(){
            this(null,null);
        }
        @Override
        public String toString(){
            return e.toString();
        }
    }

    //指向虚拟头结点的引用
    private Node dummyHead;
    private int size;

    public DummyHeadLinkedList(){
        //虚拟头结点中e=null
        dummyHead=new Node();
        size=0;
    }
}

添加结点

  • 因为有虚拟头结点,在0位置插入不用作特殊考虑
    //因为索引为0的结点前面还有一个虚拟头结点,所以不用作特殊考虑
    public void add(int index,E e){
        if(index<0||index>size){
            throw new IllegalArgumentException("add failed,illegal index");
        }
        Node prev=dummyHead;
        //这里由index-1变成了index
        for(int i=0;i<index;i++){
            prev=prev.next;
        }
        prev.next=new Node(e,prev.next);
        size++;
    }

    //在链表头部添加节点,复用add()
    public void addFirst(E e){
       add(0,e);
    }

    //在链表末尾添加元素
    public void addLast(E e){
        add(size,e);
    }

删除结点

在这里插入图片描述

    //删除链表第index个元素,在链表中这不是一个常用的操作,只作练习使用
    /**
     * 和add()一样要用到前一个结点,所以虚拟头结点又发挥作用了
     * 1、找到待删除元素前一个元素,用prev指向它
     * 2、用delNode指向待删除元素
     * 3、prev.next=delNode.next; delNode.next=null
     */
    public E remove(int index){
        if(index<0||index>=size){
            throw new IllegalArgumentException("remove failed,Illegal index");
        }
        Node prev=dummyHead;
        for(int i=0;i<index;i++){
            prev=prev.next;
        }
        Node delNode=prev.next;
        prev.next=delNode.next;
        //让这个被删除的结点可以被回收
        delNode.next=null;
        size--;
        return delNode.e;
    }

    //删除链表第一个结点
    public E removeFirst(){
        return remove(0);
    }
    //删除链表最后一个结点
    public E removeLast(){
        return remove(size-1);
    }

	// 从链表中删除元素e
    public void removeElement(E e){

        Node prev = dummyHead;
        while(prev.next != null){
            if(prev.next.e.equals(e))
                break;
            prev = prev.next;
        }

        if(prev.next != null){
            Node delNode = prev.next;
            prev.next = delNode.next;
            delNode.next = null;
            size --;
        }
    }

其他函数

    //查找链表第index个元素,在链表中这不是一个常用的操作,只作练习使用
    public E get(int index){
        if(index<0||index>=size){
            throw new IllegalArgumentException("get failed,index illegal");
        }
        Node cur=dummyHead;
        for(int i=0;i<index+1;i++){
            cur=cur.next;
        }
        return cur.e;
    }

    //更新链表第index个元素,在链表中这不是一个常用的操作,只作练习使用
    public void set(int index,E e){
        if(index<0||index>=size){
            throw new IllegalArgumentException("get failed,index illegal");
        }
        Node cur=dummyHead;
        for(int i=0;i<index+1;i++){
            cur=cur.next;
        }
        cur.e=e;
    }

    //查找是否含有元素e
    public boolean contains(E e){
        Node cur=dummyHead.next;
        while(cur!=null){
            if(cur.e.equals(e))
                return true;
            cur=cur.next;
        }
        return false;
    }

    //获取链表第一个元素
    public E getFirst(){
        return get(0);
        //return dummyHead.next.e;
    }
    //获取链表最后一个元素
    public E getLast(){
        return get(size-1);
    }

    @Override
    public String toString(){
        StringBuilder res=new StringBuilder();
        res.append(String.format("LinkedList size:%d\n [",getSize()));
        //两种遍历方法
        /*Node cur=dummyHead.next;
        for(int i=0;i<getSize();i++){
            res.append(cur.e+"->");
            cur=cur.next;
        }*/
        for(Node cur=dummyHead.next;cur!=null;cur=cur.next){
            res.append(cur.e+"->");
        }
        res.append("null]");
        return res.toString();
    }
链表时间复杂度分析

在这里插入图片描述

  • 对于链表来说最好只进行链表头部的增删查,最好不进行改操作。
链表栈
  • 利用链表封装一个栈
  • 链表栈和数组栈全部操作复杂度都是O(1),所以他们的性能差不多,但链表栈更节省空间。他们性能的差异在于数组栈的扩容缩容和链表栈频繁的new对象
public class LinkedListStack<E> implements Stack<E> {

    //复用已经写好的带有虚拟头结点的链表
    private DummyHeadLinkedList<E> linkedListStack;

    public LinkedListStack(){
        linkedListStack=new DummyHeadLinkedList<>();
    }
    //入栈
    @Override
    public void push(E e) {
        linkedListStack.addFirst(e);
    }
    //出栈
    @Override
    public E pop() {
       return linkedListStack.removeFirst();
    }
    //查看栈顶元素
    @Override
    public E peek() {
        return linkedListStack.getFirst();
    }

    @Override
    public int getsize() {
        return linkedListStack.getSize();
    }
    @Override
    public boolean isEmpty() {
        return linkedListStack.isEmpty();
    }
}
链表队列

在这里插入图片描述

头尾分析

  • 上图中head和tail端插入结点都很容易
  • 上图中head端删除结点很容易,但是tail端删除结点很难(需要从头结点遍历到tail前一个结点)
  • 所以我们就把head端当作队列的队首(只删除结点),tail端当作队尾(只插入结点)

主体

public class LinkedListQueue<E> implements Queue<E> {

    private class Node{
        public E e;
        public Node next;

        public Node(E e,Node next){
            this.e=e;
            this.next=next;
        }
        public Node(E e){
            this(e,null);
        }
        public Node(){
            this(null,null);
        }
        @Override
        public String toString(){
            return  e.toString();
        }
    }

    //head指向队首结点,tail指向队尾结点
    private Node head,tail;
    private int size;

    public LinkedListQueue(){
        head=null;
        tail=null;
        size=0;
    }
}

方法

    @Override
    public boolean isEmpty(){
        return size==0;
    }
    @Override
    public int getSize() {
        return size;
    }

    //入队
    @Override
    public void enqueue(E e) {
        //考虑队列为空添加结点的情况
        if(tail==null){
            tail=new Node(e);
            head=tail;
        }else{
            tail.next=new Node(e);
            tail=tail.next;
        }
        size++;
    }

    //出队
    @Override
    public E dequeue() {
        if(isEmpty()){
            throw new IllegalArgumentException("dequeue failed,queue is empty");
        }
        Node res=head;
        //考虑删除尾结点后队列为空的情况
        if(head==tail){
            tail=null;
        }
        head=head.next;
        //使要删除的队首结点彻底脱离链表,有利于回收
        res.next=null;
        size--;
        return res.e;
    }

    @Override
    public E getFront() {
        if(isEmpty()){
            throw new IllegalArgumentException("getFront failed,queue is empty");
        }
        return head.e;
    }

时间复杂度

  • 链表队列的操作都是O(1)
  • 数组队列出队的操作是O(n),所以链表队列性能比数组队列好
  • 循环队列(扩缩容操作耗能)和链表队列(频繁new对象耗能)性能差不多,但链表队列更节省空间
其他链表

双链表

能解决单链表在删除尾结点时间复杂度为O(n)的问题

在这里插入图片描述

循环链表

LinkedList底层就是用循环双链表实现的

在这里插入图片描述

数组链表

next用来保存下一个元素的下标,当为-1时表示尾结点,适用于已知链表的长度

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值