数据结构(3)链表之单向链表

105 阅读5分钟

链表

数组的创建通常需要申请一段连续的内存空间(一整块的内存), 并且大小是固定的

链表

  • 要存储多个元素,另外一个选择就是使用链表
  • 但不同于数组,链表中的元素在内存中不必是连续的空间
  • 链表的每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(有些语言称为指针或者链接)组成

相对于数组, 链表有一些优点:

  • 内存空间不是比是连续的. 可以充分利用计算机的内存. 实现灵活的内存动态管理.
  • 链表不必在创建时就确定大小, 并且大小可以无限的延伸下去.
  • 链表在插入和删除数据时, 时间复杂度可以达到O(1). 相对数组效率高很多.

相对于数组链表缺点:

  • 链表访问任何一个位置的元素时,都需要从头开始访问.(无法跳过第一个元素访问任何一个元素).
  • 无法通过下标直接访问元素,需要从头一个个访问,直到找到对应的位置.

链表元素的结构

image.png

链表元素的结构
this.data = 数据
this.next  = null

封装一个链表元素结构的类

//链表元素的结构
        class ListNode {
            constructor(data) {
                //创建的节点让其数据指向实参传入的值
                this.data = data;
                //新节点的下一个指针先指向空
                this.next = null;
            }
        }

单向链表的结构

image.png

链表中常见的操作

  • append(element):向列表尾部添加一个新的项
  • insert(position, element):向列表的特定位置插入一个新的项。
  • remove(element):从列表中移除一项。
  • indexOf(element):返回元素在列表中的索引。如果列表中没有该元素则返回-1
  • removeAt(position):从列表的特定位置移除一项。
  • isEmpty():如果链表中不包含任何元素,返回true,如果链表长度大于0则返回false
  • size():返回链表包含的元素个数。与数组的length属性类似。
  • toString():由于列表项使用了Node类,就需要重写继承自JavaScript对象默认的toString方法,让其只输出元素的值。

封装一个类实现单向链表的结构

 class Linelist {
            constructor() {
            //单向链表最初this.head指向空,链表长度为0
                this.head = null;
                this.length = 0;
            }
            //1.向列表尾部添加一个新的项
            append(el) {
                //创建新节点 
                let newnode = new ListNode(el);
                if (this.head == null) {
                    //空链表中新添加的数据是唯一的节点,让头部节点指向新节点
                    this.head = newnode;
                } else {
                    //链表不为空, 需要向其他节点后面追加节点
                    //定义变量用于保存从头部开始访问直到找到的当前节点,开始时变量就为头部节点
                    let current = this.head;
                    //当current.next为null时,while结束条件判断就为false,current就是最后一个节点,否则一直从头部节点开始寻找直到找到最后一个节点
                    while (current.next) {
                        current = current.next
                    }
                    //循环结束current就是链表的最后一项, 将其next赋值为新节点
                    current.next = newnode;
                }
                //无论是执行if语句还是else语句链表长度都增加1
                this.length++;
            }
            //2.向列表的特定位置插入一个新的节点。
            insert(position, element) {
                //先判断指定的位置是否合法即检测越界问题: 越界就执行if语句插入失败,Number.isInteger(position)判断是否为整数,是整数该方法返回true,不是整数返回false执行if语句就表示插入失败
                if (position < 0 || position > this.length || !Number.isInteger(position)) {
                    //位置不合法返回false
                    return false;
                }
                //当位置合法了,就插入新节点
                //创建新节点
                let newele = new ListNode(element);
                //在头部位置插入
                if (position == 0) {
                    if (this.head == null) {
                        // 如果是空链表,就让头部指向新节点
                        this.head = newele;
                    } else {
                        // 不是空链表先让新节点的next指向头部再让头部指向新节点
                        newele.next = this.head;
                        this.head = newele;
                    }
                    //最后链表长度加1
                    this.length++;
                } else if (position == this.length) {
                    //在尾部位置插入,就直接调用append方法
                    this.append(element);
                } else {
                    //在任意位置插入,需要先找到这个指定的任意位置的前一个节点
                    //current用于保存寻找的节点最初是头部元素
                    let current = this.head;
                    //index用于保存查找的索引,初始状态为头部的位置
                    let index = 0;
                    //position-1是指定的任意位置的前一个节点位置
                    while (index < position - 1) {
                        // index<position-1当布尔判定为true就表示未找到这个指定的任意位置的前一个节点则继续循环一点点向下找,当布尔判定是false表示index就是指定的任意位置而current就是指定的任意位置的前一个节点,结束while循环
                        current = current.next;
                        index++;
                    }
                    //结束循环current就是指定的任意位置的前一个节点,将新节点的next指向前一个节点的下一个节点, 将前一个节点的next指向新的节点
                    newele.next = current.next;
                    current.next = newele;
                    //链表长度加1
                    this.length++;
                }
            }
            //从列表的特定位置移除一项
            removeAt(position) {
                //需要先判断指定的位置是否合法,this.length-1表示链表最后一个节点的索引
                if (position < 0 || position > this.length - 1 || !Number.isInteger(position)) {
                    //位置不合法返回false
                    return false;
                }
                //指定位置合法
                if (this.head == null) {
                    //空链表中移除元素,直接return函数
                    return;
                } else {
                    // 定义变量, 保存信息
                    let current = this.head;
                    let index = 0;
                    let pre=null;//pre用于保存移除的指定位置的节点
                    //非空链表中移除指定位置的节点
                    if (position == 0) {
                        //移除头部节点,就让头部节点指向头部节点的next(把头部节点的下一个节点next赋值给头部节点)
                        pre=this.head;//pre用于保存移除的指定位置的节点
                        this.head = current.next;
                    } else {
                        //移除其他位置的节点包括尾部,需要先找到指定位置的前一个节点
                        while (index < position - 1) {
                            current = current.next;
                            index++;
                        }
                        //结束循环,current就是指定位置的前一个节点,让前一个节点的下一个节点current.next指向前一个节点的下下一个节点current.next.next就可以移除指定位置的节点current.next
                        pre=current.next;//current.next就是要移除的指定位置的节点
                        current.next = current.next.next;
                    }
                    //链表长度减1
                    this.length--;
                    //返回移除的数据
                    return pre.data;
                }
            }
            //返回元素在列表中的索引。如果列表中没有该元素则返回`-1`
            indexOf(element) {
                //需要从链表头部开始寻找,用current变量保存头部节点,index变量保存索引位置
                let current = this.head;
                let index = 0;
                while (index < this.length) {
                    //使用while循环遍历链表,结束条件就是遍历完所有链表节点,则index大于等于链表长度就结束
                    if (current.data == element) {
                        //当前遍历的链表节点与传入的元素相等就返回当前索引
                        return index;
                    } else {
                        //当前遍历的链表节点与传入的元素不相等就继续遍历链表
                        current = current.next;
                        index++;
                    }
                }
                //当遍历完所有链表节点都没有相等的返回-1
                return -1;
            }
            //从列表中移除指定的一项
            remove(element) {
                //先找到指定节点的索引
                let index = this.indexOf(element);
                //再根据索引位置删除节点
                return this.removeAt(index)
            }
            //如果链表中不包含任何元素,返回`true`,如果链表长度大于0则返回`false`
            isEmpty(){
                //length为0做布尔判定是false执行else语句
                if(this.length){
                    return true;
                }else{
                    return false;
                }
            }
            //返回链表包含的元素个数。与数组的`length`属性类似
            size(){
                return this.length;
            }
            //由于列表项使用了`Node`类,就需要重写继承自JavaScript对象默认的`toString`方法,让其只输出元素的值。
            toString(){
                //从头部开始遍历完所有元素拼接到新的字符串
                let current=this.head;
                let index=0;
                let arr=[];
                while(index<this.length){
                    arr.push(current.data);//遍历一次就将节点的数据添加到数组中
                    current=current.next;//current保存当前遍历到的节点的下一个节点
                    index++;
                }
                return arr.join("-");//将数组转换为字符串返回
            }
        }

单向链表结构:空链表中append

image.png

单向链表结构:非空链表中append

image.png

单向链表结构:空链表中头部位置insert

image.png

单向链表结构:非空链表中头部位置insert

image.png

单向链表结构:链表中任意位置insert

image.png

单向链表结构:链表中移除指定位置的元素是头部位置

image.png

image.png

将两者合并就是this.head = this.head.next;因为移除头部节点,就让头部节点指向头部节点的next,当链表中只有一个元素时,移除头部元素就是让头部元素的next指向null,而链表中只有一个元素时头部元素的next就是null。

单向链表结构:链表中移除指定位置的元素是尾部位置

image.png

单向链表结构:链表中移除指定位置的元素是任意位置

image.png

单向链表结构:链表中查找指定元素的索引

image.png