数据结构:链表(设计原则:对外部用户屏蔽底层的实现细节)

在这里插入图片描述

在这里插入图片描述
(1)链表动态数据结构:需要存储多少数据,就可以生成多少个节点
(2)节点类设计,跟数组不同,数组由一个个数据组成;链表由一个个结点构成;虽然是结点,可以把它看作一种稍复杂的数据;E是真正的数据,next是指向Node的一个引用(Node类型的引用)

在这里插入图片描述
在这里插入图片描述
(1)链表和数组的底层机制不一样:数组开辟连续分布的空间,可使用索引直接访问,链表的每个节点所在的内存位置是不同的,必须靠next找到元素

链表的实现:与其他线性结构类似,都使用size记录元素个数,需要维护size

节点内部类

//节点类设计,跟数组不同,数组由一个个数据组成;链表由一个个结点构成,
// 虽然是结点,可以把它看作一种稍复杂的数据,内部类
private class Node{
    //设置成public,则在LinkedList外面可以随意的修改访问
    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();
    }
}

LinkedList的其他成员变量

在这里插入图片描述

访问链表中的所有节点,必须把链表头存储起来

private Node head;
private int size;
public LinkedList() {
    head=null;
    size=0;
}
//获取链表中的元素个数
public int getSize(){
    return size;
}

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

在链表头部添加节点:构造新节点,next指向头节点,修改head指向新节点(注意引用的赋值表示引用指向同一区域)

(1)在数组中添加元素时在尾部最方便因为size指向数组最后一个元素的下一个位置,即下一个元素待添加的位置,size变量会跟踪数组的尾
(2)在链表头添加元素最方便head变量跟踪链表头而没有相应的变量跟踪链表的尾
在这里插入图片描述
过程在一个函数中执行,当函数结束之后,node节点的作用域就结束了
在这里插入图片描述

 //为链表头添加元素
    public void addFirst(E e){
        //创建新的节点
//        Node node=new Node(e);
//        node.next=head;
//        head=node;
        //首先实例化Node,直接指向链表的head,再将node赋给head
        head=new Node(e,head);
        size++;
    }

在链表中间添加节点:对链表来说其实没有索引,只做练习,在索引为n的位置添加元素,即在索引为n的结点之前添加结点,head指向的结点索引为0,在头结点添加元素,就是在索引为0的位置添加元素,跟在其他索引位置添加元素的方法不同

在这里插入图片描述

关键是要找到要添加的节点的前一个节点,当需要将元素添加在索引为0的位置时,没有前一个节点,需要处理

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

//在链表的index位置添加新的元素,在链表中很少使用索引,只作练习理解
    //搜索新节点要插入位置之前的节点,使用prev遍历
    public void add(E e,int index){
        if(index<0||index>size){
            throw new IllegalArgumentException("add failed");
        }
        //当需要将元素添加在索引为0的位置时,没有前一个节点
        if(index==0)
            addFirst(e);
        else {

            Node prev = head;
            for(int i=0;i<index-1;i++){
                //把当前prev存的节点的下一个节点放进prev中,prev向前移动
                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(e,size);
    }

在这里插入图片描述

在链表头部添加虚拟头结点:在向链表的任意位置添加元素时,在链表头添加和其他位置添加有区别;在添加元素时要找到待添加位的之前的节点,但链表头没有前面一个节点,所以在逻辑上特殊,为了统一操作,使用链表的虚拟头结点

在这里插入图片描述
(1)虚拟头结点不存储任何元素
(2)对链表来说,第一个元素是dummyHead.next对应的结点的元素,dummyHead对用户来说是没有意义的

public LinkedList() {
  //  head=null;
    dummyHead=new Node(null,null);
    size=0;
}

(1)head中会存放一个具体的元素,所以初始化时是空的head=null;
(2)引入虚拟结点,dummyHead不应该为空,应该有一个结点dummyHead=new Node(null,null);
(3)此时对一个空的链表来说是存在一个结点的,即虚拟头结点

 public void add(E e,int index){
        if(index<0||index>size){
            throw new IllegalArgumentException("add failed");
        }
        //当需要将元素添加在索引为0的位置时,没有前一个节点
//        if(index==0)
//            addFirst(e);
//        else {

            Node prev = dummyHead;
        //    for(int i=0;i<index-1;i++){
        注意index范围
        for(int i=0;i<index;i++){
                //把当前prev存的节点的下一个节点放进prev中,prev向前移动
                prev=prev.next;
            }
//            Node node = new Node(e);
//            node.next = prev.next;
//            prev.next = node;
            prev.next=new Node(e,prev.next);
      //  }
        size++;
    }

链表的遍历、查询、修改

遍历元素:需要遍历链表中每一个元素,而插入操作的遍历是需要找index的前一个位置相应的结点,从 Node prev = dummyHead开始遍历;而现在的遍历就是要遍历index位置,所以Node cur=dummyHead.next;从索引为0的位置开始(根据目标的不同设定)

//获得链表的第index个位置的元素
public E get(int index){
    if(index<0||index>=size){
        throw new IllegalArgumentException("get failed");
    }
    Node cur=dummyHead.next;
    for(int i=0;i<index;i++)
      cur=cur.next;
    return cur.e;
}

在这里插入图片描述

修改元素

//修改链表的第index位置的元素
public void set(int index,E e){
    if(index<0||index>=size){
        throw new IllegalArgumentException("set failed");
    }
    Node cur=dummyHead.next;
    for(int i=0;i<index;i++){
        cur=cur.next;
    }
    cur.e=e;
}

查找元素:没有索引参数了,需要从头进行一次遍历,cur!=null;cur是一个有效结点

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

另一种从头遍历的写法:用for循环替代while循环,不涉及size,即不涉及循环多少次,循环条件还是cur!=null

在这里插入图片描述

测试

在这里插入图片描述

删除元素:删除索引为2的元素,找到待删除元素之前的结点,所以使用prev

在这里插入图片描述

跳过2结点,等同于删除了2结点
在这里插入图片描述

在这里插入图片描述

public E delete(int index){
    if(index<0||index>=size){
        throw new IllegalArgumentException("delete failed");
    }
    Node prev=dummyHead;
    for(int i=0;i<index;i++){
        //把当前prev存的节点的下一个节点放进prev中,prev向前移动
        prev=prev.next;
    }
    Node retNode=prev.next;
    prev.next=retNode.next;
    retNode.next=null;//注意
    size--;
    return retNode.e;
}~

在这里插入图片描述

链表时间复杂度分析

在这里插入图片描述
在这里插入图片描述
链表的修改操作需要从头遍历

在这里插入图片描述
查找方法需要从头遍历
在这里插入图片描述
在这里插入图片描述
相比链表,数组有索引可以快速访问,链表没有这种优势,但如果只对链表头进行操作,效率高,所以对链表进行如下操作;对链表来说,与数组类似,有size来记录元素个数,但由于链表元素不是在一片连续空间,即使知道size,也需要遍历一遍链表才能找到最后一个结点,因此链表不适合在链表尾进行操作(不适合需要遍历的操作),而数组适合
在这里插入图片描述

在这里插入图片描述
起始的dummyHead的next为null,当插入第一个结点时,node.next=prev.next;即为null,接下来不断在链表头插入元素,最后一个结点的next依然为null,因此判断当前结点是否是最后一个,使用next=null判断

由链表适合于在链表头进行操作,而栈是先进后出,只对栈顶进行操作,那么可把链表头当作栈顶,用链表作为栈的底层实现

在这里插入图片描述

public class LinkedListStack<E> implements Stack<E>{
    private LinkedList<E> linkedList;
    public LinkedListStack(){
        linkedList=new LinkedList<>();
    }
    @Override
    public void push(E e) {
        //父类向子类转型,需要强制转型
        linkedList.addFirst(e);
    }

    @Override
    public E pop() {
        return linkedList.delete(0);
    }

    @Override
    public E top() {
        return linkedList.get(0);
    }

    @Override
    public int getSize() {
        return linkedList.getSize();
    }

    @Override
    public boolean isEmpty() {
        return linkedList.isEmpty();
    }

   
}

在这里插入图片描述

在这里插入图片描述
对于数组栈来说,经常需要进行resize动态扩容缩容,而链表不存在这样的情况,链表栈与数组栈的耗时多少跟操作数的多少以及系统有关,链表栈用时不一定比数组栈要快,这种比较是是否复杂的,这是因为链表栈中包含更多的new Node操作,通常操作越多,new操作耗时越明显,数组栈需要进行resize
在这里插入图片描述
因此链表栈和数组栈在时间复杂度上没有巨大的差异,而循环队列和数组队列的差异是巨大的

链表实现队列

(1)使用链表实现队列,需要在一端插入元素,在另一端删除元素,在线性结构的两端同时操作,那么用一端操作的复杂度是O(n)级别的,在用数组实现队列时遇到这种问题,采用循环队列改进;因此在用链表实现队列时,也需要进行改进
(2)对链表来说,在头部插入删除元素都是容易的,这是因为有head这个遍历来标记链表的头,同理要是在尾部进行操作方便,创建变量标记尾部tail
在这里插入图片描述
(3)在尾部添加元素,相当于在链表索引为size的位置添加元素,tail指向的结点是待添加位置之前的结点,从两端添加节点都很容易
(4)在尾部删除元素,需要找到待删除元素之前的结点,因此仍然需要遍历一遍链表,因此无法使用O(1)复杂度的操作删除tail位置的结点,从tail删除元素不容易
(5)因此在使用链表实现队列时,head端作队首,tail端作队尾,从head端删除元素,从tail端插入元素

在这里插入图片描述
(6)由于链表的操作都在链表的一侧完成,不再使用虚拟头结点,这是因为不涉及对链表中间一个元素进行插入或删除,没必要统一对链表中间和两侧元素的操作,没有使用dummyHead
在这里插入图片描述

队列为空时,在队尾入队一个结点,注意要同时维护head与tail指向同一个结点, 维护tail tail=node;维护head head=tail;队列出队后为空时,注意要同时维护head和tail,因为经过以上操作,不维护tail,tail指向的依然是res,此时tail应该为空,tail=null;(当列表只有一个元素时,tail=node;head=tail;当列表为空时,head=null;tail=null;)

出队元素时,需要将其与链表断开res.next=null;

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

    private class Node{
        //设置成public,则在LinkedList外面可以随意的修改访问
        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 Node tail;
    private int size;

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


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

    public E getFront(){
        if(isEmpty())
            throw new IllegalArgumentException("get failed");
        return head.e;
    }

    public int getSize() {
        return size;
    }

    public String toString() {
        StringBuilder res=new StringBuilder();
        res.append("head: ");
        res.append('[');
        Node cur;
        for(cur=head;cur!=null;cur=cur.next){
            res.append(cur.e+"->");
        }
        res.append("] tail->null");
        return res.toString();
    }


    @Override
    public void enqueue(E n) {
        //创建新的节点
        Node node=new Node(n);
        if(tail==null){
            //维护tail
            tail=node;
            //维护head
            head=tail;}
        else{
            //只需要维护tail
            tail.next=node;
            tail=node;}
        size++;
    }

    @Override
    public E dequeue() {
        if(isEmpty())
            throw new IllegalArgumentException("remove failed");

        Node res= head;
        head=head.next;
        //经过以上操作,res的next相当于指向了head结点
        //需要将res从链表中断开
        res.next=null;
        //如果列表出队后没有元素了
        if(head==null)
            //因为经过以上操作,不维护tail,tail指向的依然是res,此时tail应该为空
            tail=null;
        size--;
        return res.e;
    }


}

循环队列和链表队列的时间复杂度是一样的

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值