数据结构之——链表

课程《玩转数据结构》学习java

  • 链表与数组
  • 使用虚拟头节点
  • 链表的增删改查
  • 链表的时间复杂度分析
  • 使用链表实现栈与队列

链表与数组

链表是典型的线性动态数据结构,也是学习树形数据结构的敲门砖。与数组不一样,链表的意义在于动态二字。再回顾一下什么是数组:在内存中开辟一段连续的存储空间的相同数据类型元素存储的集合 。数组并不具有动态的能力,为了让数组具备动态的特性,咱们能够实现本身的数组,让其具有自动扩容以及缩容(resize)的能力。动态数组
而对于,与队列这两种具有特殊功能的线性数据结构,可使用数组做为底层原理来实现。对于栈的特性即LIFO,使用动态数组做为底层实现知足了栈各个功能的时间复杂度为O(1)。而队列的特性为:FIFO,若是使用数组做为底层,在队列的出队操做时,这一项功能的时间复杂度就为O(n)。使用循环队列的思想,则能够将出队操做优化至O(1)。
链表则是一种真正的动态数据结构。由于数组在内存的空间是连续的,因此最大的优点 是支持“随机访问”,而链表最大的优势则是“真正的动态”。链表不会浪费多余的内存空间,不须要处理容量的问题,可是也丧失了数组的随机访问的能力。
node


上图表示的就是一个链表,图中的圆形表示为链表中的一个节点,每一个节点都存储着下一个节点的引用。链表的尾部,也就是链中最后一个节点,是没有指向下一个节点的,因此天然指向了Null。要想实现 “一个节点存储着下一个节点的引用”功能并不难,咱们只须要在链表类中,增长一个内部类Node便可:

public class LinkedList<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 head;
    private int size;
    public LinkedList(){
        head = null;
        size = 0;
    }
    //  获取链表中元素的个数
    public int getSize(){
        return size;
    }

    // 判断链表是否为空
    public boolean isEmpty(){
        return size==0;
    }
}
复制代码

链表中每个节点都存储着下一个节点的引用,那么谁来存储链表头部的引用呢?因此,与数组不一样,链表须要额外去维护一个变量,这个变量咱们称做head,用于存储链表头的引用。git

使用虚拟头节点

如今向链表添加元素。
咱们须要考虑两种状况,第一种状况为:向链表头部添加元素。
github


在向链表头部添加元素时,咱们须要:

1:newNode.next = head;// 将添加的节点的next指向head
2: head = newNode;// 将head再次指向头部
复制代码

实现代码为:数组

public void addFirst(E e){
    head = new Node(e,head);
    size++;
}
复制代码

还有一种状况是:在链表任意位置添加元素,这一点和在链表头部添加元素略有不一样。(广泛来说,当你选择了链表这种数据结构时,每每不会涉及向链表的中间添加元素,实现此功能是为了更加深刻地学习链表)
bash


咱们考虑一下,在链表中间插入元素时,假设插入位置的"索引"称做index。咱们首先须要将插入的节点指向index处的节点,本图为向index==2的位置插入元素99。而后再将index位置前的一个节点指向被插入的节点,那么咱们如何获取index-1处的节点呢?答案就是遍历。咱们须要一个特殊的变量,让它指向index-1处的节点,如今用prev表示这个变量,最初让 prev=head,每次让 prev=prev.next,遍历index-1次,就能够得到index-1处的,也就是待插入位置的前一个位置的索引处的节点。插入的过程为:

1: newNode.next = prev.next
2: prev.next = newNode
复制代码

代码为:数据结构

public void add(int index,E e){
    if(index<0 || index>size)
        throw new IllegalArgumentException("Index is Illegal");
    if(index==0){
        // 若是在链表头部添加元素
        addFirst(e);
    }else{
        Node prev = head;
        for(int i=0;i<index-1;i++){
            prev = prev.next;
        }
        prev.next = new Node(e,prev.next);
        size++;
    }
}
复制代码

若是使用head这个变量去维护链表头天然是能够的,可是咱们看到了,咱们的链表在头部添加元素时,和在其余位置添加元素的思路是不同的。有没有办法可以将链表进行优化,使得链表的头部同链表的其余位置在增删改查的操做一致呢?使用虚拟头节点就能够优化链表,解决这样的一个问题。
ide


如上图所示,咱们在本来的head前使用一个dummyHead这样的一个变量,让它指向本来的head,这样对于咱们来说,链表中全部的节点都知足了“有指向它的节点”这样一个特性。

链表的增删改查

有了dummyHead虚拟头节点后,链表的增删改查都会变的很是容易。post

向链表中添加元素

public void add(int index,E e){
    if(index<0 || index>e)
        throw new IllegalArgumentException("Index is Illegal");
    Node prev = dummyHead;
    for(int i=0;i<index;i++){
        prev = prev.next;
    }
    prev.next = new Node(e,prev.next);
    size++;
}
// 在链表头添加新的元素e
public void addFirst(E e){
    add(0,e);
}
// 在链表尾添加新的元素e
public void addLast(E e){
    add(size,e);
}
复制代码

向链表中删除元素



public E remove(int index){
    if(index<0 || index>=size)
        throw new IllegalArgumentException("index is Illegal");
    Node prev = dummyHead;
    for(int i=0;i<index;i++){
        prev = prev.next;
    }
    E delNode = prev.next;
    prev.next = prev.next.next; // prev.next = delNode.next;
    delNode.next = null;
    return delNode.e;
}
// 从链表中删除第一个元素,并返回
public E removeFirst(){
    return remove(0);
}
// 从链表中删除最后一个元素,并返回
public E removeLast(){
    return remove(size-1);
}
复制代码

向链表中查询及修改元素

// 改
public void set(int index,E e){
    if(index<0 || index>=size)
        throw new IllegalArgumentException("Index is Illegal");

    Node prev = dummyHead;
    for(int i=0;i<index;i++){
        prev = prev.next;
    }
    prev.next.e = e;
}
// 查
public E get(int index){
    if(index<0 || index>=size)
        throw new IllegalArgumentException("Index is Illegal");
    Node prev = dummyHead;
    for(int i=0;i<index;i++){
        prev = prev.next;
    }
    return prev.next.e;
}
// 得到链表的第一个元素
public E getFirst(){
    return get(0);
}
// 得到链表的最后一个元素
public E getLast(){
    return get(size-1);
}
复制代码

代码连接学习

链表的时间复杂度分析

咱们如今来看一下链表的增删改查各个操做的时间复杂度:

  • 向链表中添加元素
    与动态数组相反,在动态数组的末尾添加元素的时间复杂度为O(1),在头部添加元素则须要将数组总体向后挪动,须要O(n)的时间复杂度。链表的添加元素操做中,在链表头添加元素的时间复杂度为O(1),在链表尾部添加元素,须要将链表遍历一遍,因此时间复杂度则为O(n)。
  • 向链表中删除元素
    在链表头部删除元素很是简单,时间复杂度为O(1)。删除链表尾部仍是须要将链表总体遍历,因此时间复杂度为O(n)。
  • 链表中查询元素
    链表不具有数组的下标索引这种快速查询的机制,因此对于链表来讲,查询元素的时间复杂度为O(n)。由于只有将链表进行遍历,才能知道链表中是否有你想要查询的元素,对于链表来讲,查询元素这个功能是不利的,事实上,也确实如此。选择了链表这种数据结构主要的操做都是在增删上,而数组这种数据结构则更加适合查询操做,由于数组的索引特性使得查询操做为O(1)的时间复杂度。
  • 链表中修改元素
    对于链表的修改元素这一操做来讲,在链表头操做的时间复杂度为O(1),在链表尾修改元素的时间复杂度则是O(n)。

使用链表实现栈与队列

栈与队列是两种特殊的线性数据结构,它们都是基于某种线性数据结构做为底层进行实现的。动态数组做为底层能够实现栈与队列,而且咱们使得栈这种数据结构的各个操做均为O(1)的时间复杂度,而队列在使用数组做为底层实现时,出队操做的时间复杂度为O(n),可是循环队列则作出了改进,将队列的各个操做优化至O(1)。咱们再回顾一下栈与队列的接口方法:
Stack

public interface Stack<E> {
    // 入栈
    void push(E e);
    // 出栈
    E pop();
    // 查看栈顶元素
    E peek();
    int getSize();
    boolean isEmpty();
}
复制代码

Queue

public interface Queue<E> {
    // 入队
    void enqueue(E e);
    // 出队
    E dequeue();
    // 查看队首的元素
    E getFront();
    int getSize();
    boolean isEmpty();
}
复制代码

若是将栈与队列的底层变为链表,那么如何进行实现呢?

LinkedListStack

对于链表来讲,在链表头操做元素均为O(1)的时间复杂度,而栈是一种仅在栈顶进行push与pop的特殊的数据结构。因此咱们的思路很是简单,将链表头做为栈顶就可使得栈的相关操做为O(1)的时间复杂度了,由于代码比较简单,因此直接给出连接,再也不叙述:连接

LinkedListQueue

队列和栈不一样,由于FIFO的这种特性,就须要在队列的两头进行操做(从一端添加元素,从另外一端删除元素)。对于数组和链表两种数据结构来讲,不管是哪种,在两端进行操做的时间复杂度必是O(1)和O(n)。对于数组来讲,咱们使用了循环队列这种思想对出队操做进行优化,对于链表也必然有优化的方法,试想一下,在链表头部进行操做的时间复杂度为O(1),若是在链表的尾部也添加一个变量进行维护,那么每次在添加元素时,只须要让尾部指向新添加的元素,而且再次让维护链表尾部的这个变量指向最后一个元素不就能够了吗?假设维护链表尾部的这个变量叫作"tail",在每次向链表中添加元素时,咱们只须要tail.next = newNode;tail = newNode就能够了,这样在链表尾部添加元素就会变为一个时间复杂度为O(1)的操做。


而链表头不管是删除元素仍是添加元素都是O(1),咱们能够将链表尾部变为队列尾,将链表头看成队列头。 咱们只看入队操做和出队操做:

// 链表为底层的队列:入队
@Override
public void enqueue(E e){
    Node node = new Node(e);
    if(isEmpty()){
        head = node;
        tail = node;
    }else{
        tail.next = node;
        tail = tail.next;
    }
    size++;
}
复制代码
// 链表为底层的队列:出队
@Override
public E dequeue(){
    if(isEmpty())
        throw new IllegalArgumentException("Queue is Empty");
    Node retNode = head;
    if(head==tail){
        head = null;
        tail = null;
    }else{
        head = head.next;
    }
    size--;
    retNode.next = null;
    return retNode.e;
}
复制代码

代码连接

相关文章
相关标签/搜索