数据结构与算法系列(一)—链表

数据结构与算法系列(一)—链表

前言

大家好,本系列开始讲结合刷力扣的过程,对数据结构和算法进行总结。牧码心今天给大家推荐一篇数据结构与算法系列(一)—链表的文章,希望对你有所帮助。大纲如下:

  • 链表概要
  • 链表分类
  • 链表实现

链表概要

链表是线性表的一种,相比于数组,链表是一种稍微复杂一点的数据结构,为了更好的理解链表和数组,我们先来看下这两者的区别:

  • 存储结构
    数组需要一块连续的内存空间来存储,对内存的要求比较高。如果我们申请一个 100MB 大小的数组,当内存中没有连续的、足够
    大的存储空间时,即便内存的剩余总可用空间大于 100MB,仍然会申请失败。链表并不需要一块连续的内存空间,它通过“”指针“” 将一组零散的内存串联起来使用,所以如果我们申请的是 100MB 大小的链表,根本不会有问题。对比结构如图所示:
    链表与数组存储模型
  • 基本操作
    数组和链表都支持查询,删除,插入等操作,但因为内存存储机制的不同,它们插入、删除、随机访问操作的时间复杂度也不同.数组的插入、删除操作时,为了保持内存数据的连续性,需要做大量的数据移动,所以时间复杂度 O(n),而在链表中插入或者删除一个数据,我们并不需要为了保持内存的连续性而数据移动,所以对应的时间复杂度是 O(1)。但是,链表要想随机访问第 k 个元素,就没有数组那么高效了。因为链表中的数据并非连续存储的,所以无法像数组那样,根据首地址和下标,通过寻址公式就能直接计算出对应的内存地址,而是需要根据指针一个结点一个结点地依次遍历,直到找到相应的结点,对应的时间复杂度是 O(n)。而数组可以根据下标随机访问,对应的时间复杂度是 O(1)。对比如下:
时间复杂度链表数组
插入/删除O(1)O(n)
随机访问O(n)O(1)

综上分析,链表是通过指针将一组零散的内存块串联在一起,其中内存块也可以成为结点,一般结点由数据域(data)和指针域(next)构成,指针(next)用于指向其他结点的地址。

链表的类型

链表按照结构可以分为单链表,双向链表和循环链表等类型的链式结构。每种类型的链表各自有基本操作特点,下面我们分别介绍:

  • 单链表
    单链表由节点组成,每个节点都包含下一个节点的指针,没有环形结构。其结构示意图如下:
    单链表结构示意图
    从图看出有两个节点比较特殊,一个是头结点,用来于记录链表的基地址,不存储数据,也是遍历整个链表的起始位置。一个是尾结点,其指针不是指向下一节点,而是指向一个空地址。下面我们用图演示,单链表的操作,如插入,删除等。
  • 单链表添加节点
    单链表添加节点

说明:在"节点10"与"节点20"之间添加"节点15"

  • 单链表删除节点
    单链表删除节点

删除"节点30",删除之后,“节点20” 的后继节点为"节点40"。

  • 双向链表
    双向链表也是由节点组成,它的每个数据节点都有两个指针,分别指向直接后继(next)和直接前驱(pre)。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。其示意结构图如下:
    双向链表示意图
    从图中可以看出,双向链表需要额外的两个空间来存储后继结点和前驱结点的地址。所以,如果存储同样多的数据,双向链表要比单链表占用更多的内存空间。虽然两个指针比较浪费存储空间,但可以支持双向遍历,这样也带来了双向链表操作的灵活性。下面用图演示双向链表的操作,如如插入,删除等。
  • 双链表添加节点
    双链表添加节点

说明:在"节点10"与"节点20"之间添加"节点15"

  • 双链表删除节点
    双链表删除节点

说明:删除"节点30"

  • 循环链表
    循环链表是一种特殊的单链表。实际上,循环链表也很简单。它跟单链表唯一的区别就在尾结点。我们知道,单链表的尾结点指针指向空地址,表示这就是最后的结点了。而循环链表的尾结点指针是指向链表的头结点。从图中可以看出来,它像一个环一样首尾相连,所以叫作“循环”链表。其结构示意图如下:
    循环链表结构图
    和单链表相比,循环链表的优点是从链尾到链头比较方便。当要处理的数据具有环型结构特点时,就特别适合采用循环链表。比如著名的约瑟夫问题。

链表实现

  • 单链表实现(java版)

/**
 * @program:com.greekw.datastruct.linkedlist
 * @desc:单链表操作demo
 * 1、单链表的插入,删除,查找
 * @Author:greekw
 * @Date:2020-06-27 18:49
 */
public class SinglyLinkedListDemo {
    // 定义一个头结点
    private static Node head=null;
    // 定义一个Node类封装结点数据
    public static class Node{
        private int data;
        private Node next;

        public Node(int data,Node next){
            this.next=next;
            this.data=data;
        }

        public int getData(){
            return data;
        }
    }
    public static Node createNode(int data){
        return new Node(data,null);
    }

    // 创建链表,头插入,头插法建立链表,输入顺序是相反的,即逆序
    public static void addAtHead(int data){
        Node new_node=new Node(data,null);
        if(head==null){
            head=new_node;
        }else {
            new_node.next=head;
            head=new_node;
        }
    }

    // 创建链表,尾插法,在链表尾部顺序插入
    public static void addAtTail(int data){
        Node new_node=new Node(data,null);
        // 空链表,则可以赋值给head
        if(head==null){
            head=new_node;
        }else {
            Node p=head;
            //寻找尾结点,尾指针不是指向下一个结点,而是指向一个空地址 NULL
            while (p.next!=null){
                p=p.next;
            }
            new_node.next=p.next;
            p.next=new_node;
        }

    }

    // 插入在指定结点前
    public static void addAtBefore(Node p,int data){
        Node new_node=new Node(data,null);
        if (p == null) return;
        if(p==head){
            addAtHead(data);
            return;
        }
        Node q=head;
        while (q!=null && q.next.data!=p.data){
            q=q.next;
        }
        if(q==null) return ;
        new_node.next=q.next;
        q.next=new_node;
    }

    // 插入在指定结点后
    public static void addAtAfter(Node p,int data){
        Node new_node=new Node(data,null);
        if (p == null) return;
        Node q=head;
        while (q!=null && q.data!=p.data){
            q=q.next;
        }
        if(q==null) return ;
        new_node.next=q.next;
        q.next=new_node;
    }

    // 遍历链表
    public static void printAt(){
        Node p=head;
        while (p!=null){
            System.out.println(p.data);
            p=p.next;
        }
    }
    // 链表查找,根据索引查找
    public static Node findByIndex(int index){
        Node p=head;
        int pos=0;
        // 遍历链表,查找与index相同的位置
        while (p!=null && pos!=index){
            p=p.next;
            ++pos;
        }
        return p;
    }

    // 链表查找,根据值查找
    public static Node findByValue(int data){
        Node p=head;
        while (p!=null && p.data!=data){
            p=p.next;
        }
        return p;
    }

    // 链表删除节点
    public static void deleteNode(Node p){
        if(p==null || head==null) return;
        // 删除头结点
        if(head==p){
            head=head.next;
            return;
        }
        // 查找要删除的节点
        Node q=head;
        while (q!=null && q.next.data!=p.data){
            q=q.next;
        }
        if(q==null) return;
        q.next=q.next.next;

    }


    // 测试用例
    public static void main(String[] args) {

        for(int i=0;i<20;i++){
            addAtTail(i*2);
        }
        //printAt();
        /*Node node=findByIndex(3);
        System.out.println(node.data);
        Node node1=findByValue(10);
        System.out.println(node1.data);*/
        addAtAfter(createNode(2),7);
        printAt();
        addAtBefore(createNode(2),11);
        printAt();
        deleteNode(createNode(2));
        printAt();
    }
}
  • 双链表实现(java版)

/**
 * @program:com.greekw.datastruct.linkedlist
 * @desc:双链表操作Demo
 * jdk基于双链表实现的可参考 {@link LinkedList}
 * @Author:greekw
 * @Date:2020-07-05 17:19
 */
public class DoubleLinkedListDemo<T>{

    // 表头
    private DNode<T> mHead;
    // 节点个数
    private int mCount;

    // 双向链表“节点”对应的结构体
    private class DNode<T> {
        public DNode prev;
        public DNode next;
        public T value;

        public DNode(T value, DNode prev, DNode next) {
            this.value = value;
            this.prev = prev;
            this.next = next;
        }
    }

    // 构造函数
    public DoubleLinkedListDemo() {
        // 创建“表头”。注意:表头没有存储数据!
        mHead = new DNode<T>(null, null, null);
        mHead.prev = mHead.next = mHead;
        // 初始化“节点个数”为0
        mCount = 0;
    }

    // 返回节点数目
    public int size() {
        return mCount;
    }

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

    // 获取第index位置的节点
    private DNode<T> getNode(int index) {
        if (index<0 || index>=mCount)
            throw new IndexOutOfBoundsException();

        // 正向查找
        if (index <= mCount/2) {
            DNode<T> node = mHead.next;
            for (int i=0; i<index; i++)
                node = node.next;

            return node;
        }

        // 反向查找
        DNode<T> rnode = mHead.prev;
        int rindex = mCount - index -1;
        for (int j=0; j<rindex; j++)
            rnode = rnode.prev;

        return rnode;
    }

    // 获取第index位置的节点的值
    public T get(int index) {
        return getNode(index).value;
    }

    // 获取第1个节点的值
    public T getFirst() {
        return getNode(0).value;
    }

    // 获取最后一个节点的值
    public T getLast() {
        return getNode(mCount-1).value;
    }

    // 将节点插入到第index位置之前
    public void insert(int index, T t) {
        if (index==0) {
            DNode<T> node = new DNode<T>(t, mHead, mHead.next);
            mHead.next.prev = node;
            mHead.next = node;
            mCount++;
            return ;
        }

        DNode<T> inode = getNode(index);
        DNode<T> tnode = new DNode<T>(t, inode.prev, inode);
        inode.prev.next = tnode;
        inode.next = tnode;
        mCount++;
        return ;
    }

    // 将节点插入第一个节点处。
    public void insertFirst(T t) {
        insert(0, t);
    }

    // 将节点追加到链表的末尾
    public void appendLast(T t) {
        DNode<T> node = new DNode<T>(t, mHead.prev, mHead);
        mHead.prev.next = node;
        mHead.prev = node;
        mCount++;
    }

    // 删除index位置的节点
    public void del(int index) {
        DNode<T> inode = getNode(index);
        inode.prev.next = inode.next;
        inode.next.prev = inode.prev;
        inode = null;
        mCount--;
    }

    // 删除第一个节点
    public void deleteFirst() {
        del(0);
    }

    // 删除最后一个节点
    public void deleteLast() {
        del(mCount-1);
    }
    // 测试用例
    public static void main(String[] args) {
        int[] iarr = {10, 20, 30, 40};

        System.out.println("\n----int_test----");
        // 创建双向链表
        DoubleLinkedListDemo<Integer> dlink = new DoubleLinkedListDemo<Integer>();

        dlink.insert(0, 20);    // 将 20 插入到第一个位置
        dlink.appendLast(10);    // 将 10 追加到链表末尾
        dlink.insertFirst(30);    // 将 30 插入到第一个位置

        // 双向链表是否为空
        System.out.printf("isEmpty()=%b\n", dlink.isEmpty());
        // 双向链表的大小
        System.out.printf("size()=%d\n", dlink.size());

        // 打印出全部的节点
        for (int i=0; i<dlink.size(); i++)
            System.out.println("dlink("+i+")="+ dlink.get(i));
    }
}

总结

总之,链表和数组的对比,在实际的软件开发中,不能仅仅利用复杂度分析就决定使用哪个数据结构来存储数据。要根据具体情况,权衡究竟是选择数组还是链表

  • 数组简单易用,在实现上使用的是连续的内存空间,可以借助 CPU 的缓存机制,预读数组中的数据,所以访问效率更高。而链表在内存中并不是连续存储,所以对 CPU 缓存不友好,没办法有效预读。
  • 数组的缺点是大小固定,一经声明就要占用整块连续内存空间。如果声明的数组过大,系统可能没有足够的连续内存空间分配给它,导致“内存不足(out of memory)”。如果声明的数组过小,则可能出现不够用的情况。这时只能再申请一个更大的内存空间,把原数组拷贝进去,非常费时。链表本身没有大小的限制,天然地支持动态扩容,我觉得这也是它与数组最大的区别。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值