数据结构(一)--- 链表

一、复杂度概述

数据结构概述:在计算机中存储和组织数据的方式。
算法概述:解决方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系统的方法描述解决问题的策略机制。
算法复杂度(时间/空间复杂度 ),大O表示法:

  • O(1) 常数阶
  • O(log(n)) 对数阶
  • O(n) 线性阶
  • O(nlog(n)) 线性和对数乘积
  • O(n2) 平方阶
  • O(2n) 指数阶
二、数组Array

线性结构(数组、链表、栈、队列和哈希表),非线性结构(树、图),抽象数据结构(集合、字典)。

数组(Array)在js中有专门的api调用,常用方法可参考JavaScript 数组参考手册 (w3school.com.cn)

三、链表LinkedList
  • 每个元素由存储元素本身的节点与一个指向下一个元素的引用组成
  • 可以灵活实现动态内存管理
  • 插入和删除操作时,时间复杂度O(1)
  • 查找时间复杂度O(n)

链表和数组一样,可以用于存储一系列的元素,但是链表和数组的实现机制完全不同。链表的每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(有的语言称为指针或连接)组成。
在这里插入图片描述

  • head属性指向链表的第一个节点;
  • 链表中的最后一个节点指向null;
  • 当链表中一个节点也没有的时候,head直接指向null;
1.数组的缺点

(1)数组的创建通常需要申请一段连续的内存空间(一整块内存),并且大小是固定的。所以当原数组不能满足容量需求时,需要扩容(一般情况下是申请一个更大的数组,比如2倍,然后将原数组中的元素复制过去)。
(2)在数组的开头或中间位置插入数据的成本很高,需要进行大量元素的位移。

2.链表的优点

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

3.链表的缺点

(1)链表访问任何一个位置的元素时,都需要从头开始访问,需要顺着指针一个一个找(无法跳过第一个元素访问任何一个元素)。
(2)无法通过下标值直接访问元素,需要从头开始一个个数组内和指针访问,直到找到对应的元素,这也是和数组最明显的区别。
(3)虽然可以轻松地到达下一个节点,但是回到前一个节点是很难的。

链表中的常见操作:
  • append(element):向链表尾部添加一个新的项;
  • insert(position,element):向链表的特定位置插入一个新的项;
  • get(position):获取对应位置的元素;
  • indexOf(element):返回元素在链表中的索引。如果链表中没有该元素就返回-1;
  • update(position,element):修改某个位置的元素;
  • removeAt(position):从链表的特定位置移除一项;
  • remove(element):从链表中移除一项;
  • isEmpty():如果链表中不包含任何元素,返回trun,如果链表长度大于0则返回false;
  • size():返回链表包含的元素个数,与数组的length属性类似;
  • toString():由于链表项使用了Node类,就需要重写继承自JavaScript对象默认的toString方法,让其只输出元素的值;
  • forwardString():返回正向遍历节点字符串形式;(双向链表)
  • backwordString():返回反向遍历的节点的字符串形式;(双向链表)
三、封装单向链表
1、创建单向链表类
// 封装单向链表类
function LinkList(){
    // 封装一个内部类:节点类,用来生成每个节点的实例
    function Node(data){
        this.data = data;
        this.next = null;
    }
    //属性
    //创建链表头,head指向链表的第一个节点
    this.head = null
    //创建链表长度
    this.length = 0;
}
2. append方法:向尾部插入节点

(1)先声明一个新的节点,判断链表是否有数据。如果没有数据,直接head指向新节点。
(2)如果有数据,那么就要声明一个变量current来存储当前的指针。这个指针首先指向第一个元素,然后循环看一下current.next是否有指向,如果有,就改变current为current.next,直到current.next为空,指向最后一个节点,说明已经到链表尾部了。
(3)最后通过current.next = newNode,将current指针指向新节点
在这里插入图片描述

LinkList.prototype.append = data => { //这里也可以写成append(data) {
	//1.创建新节点
    let newNode = new Node(data)
    //2.添加新节点
    //情况1:只有一个节点时候
    if(this.length == 0){
        this.head = newNode
        //情况2:节点数大于1,在链表的最后添加新节点  
    }else {              
        //让变量current指向第一个节点
        let current = this.head
        //当current.next(下一个节点不为空)不为空时,一直循环,直到current指向最后一个节点
        while (current.next){
            current = current.next
        }
        // 最后节点的next指向新的节点
        current.next = newNode
    }
    //3.添加完新结点之后length+1
    this.length += 1
}

测试代码:

//1.创建LinkList
let list = new LinkList()
//2.测试append方法
list.append('aaa')
list.append('bbb')
list.append('ccc')
console.log(list);  
3.toString方法:链表元素转字符串
LinkList.prototype.toString = () => {//toString() {
	// 1.定义变量
    let current = this.head
    let listString = ""
    // 2.循环获取一个个的节点
    while(current){ 
          listString += current.data + " "
          current = current.next//千万不要忘了拼接完一个节点数据之后,让current指向下一个节点
    }
    return  listString
}

测试代码:

//1.创建LinkList
let list = new LinkList()
//2.插入数据
list.append('aaa')
list.append('bbb')
list.append('ccc')
//3.测试toString方法
console.log(list.toString());// aaa bbb ccc
4. insert方法:在任意位置插入数据

情况1: 在position为0的位置插入数据,那么这样的话就要

(1)先让插入的新元素指向原来的第一个元素
(2)然后再让head指向插入的新元素。

在这里插入图片描述
情况2: 往其他位置插入

(1)首先应该拿到该位置的那个元素
(2)让新节点指向该元素
(3)让该位置前边的元素指向新节点

在这里插入图片描述

LinkList.prototype.insert = (position, data) => { //insert(position, data) {
	//理解positon的含义:position=0表示新界点插入后要成为第1个节点,position=2表示新界点插入后要成为第3个节点
	//1.对position进行越界判断:要求传入的position不能是负数且不能超过LinkList的length
	if(position < 0 || position > this.length){
		return false
	}
	//2.根据data创建newNode
	let newNode = new Node(data)
	//3.插入新节点
	//情况1:插入位置position=0
	if(position == 0){
		// 让新节点指向第一个节点
    	newNode.next = this.head
    	// 让head指向新节点
    	this.head = newNode
    //情况2:插入位置position>0(该情况包含position=length)
	} else{
	let index = 0
    let previous = null
    let current = this.head
    //步骤1:通过while循环使变量current指向position位置的后一个节点(注意while循环的写法)
    while(index++ < position){
    	//步骤2:在current指向下一个节点之前,让previous指向current当前指向的节点
        previous = current
        current = current.next
    }
    // 步骤3:通过变量current(此时current已经指向position位置的后一个节点),使newNode指向position位置的后一个节点
    newNode.next = current
    //步骤4:通过变量previous,使position位置的前一个节点指向newNode
    previous.next = newNode
    /* 
      启示:
       1.我们无法直接操作链表中的节点,但是可以通过变量指向这些节点,以此间接地操作节点(替身使者);比如current指向节点3,想要节点3指向节点4只需要:current.next = 4即可。
       2.两个节点间是双向的,想要节点2的前一个节点为节点1,可以通过:1.next=2,来实现;
     */
	}
    //4.新节点插入后要length+1
    this.length += 1;
    return true
}

测试代码:

//1.创建LinkList
let list = new LinkList()
//2.插入数据
list.append('aaa')
list.append('bbb')
list.append('ccc')  
//3.测试insert方法
list.insert(0, '在链表最前面插入节点');
list.insert(2, '在链表中第二个节点后插入节点');
list.insert(5, '在链表最后插入节点');
console.log(list.toString());// 0 aaa bbb 2 ccc 5
5.get获取某个位置的元素

传入位置返回当前位置元素。
主要的思路就是从head找,通过循环依次改变指针的指向,直到指针指向当前位置,那么就把当前位置返回,(如果用户输入0,那么直接返回this.head)
实现过程:
以获取position = 2为例:
首先使current指向第一个节点,此时index = 0;通过while循环使current循环指向下一个节点,注意循环终止的条件index++ < position,即当index = position时停止循环,此时循环了1次,current指向第二个节点(Node2),最后通过current.data返回Node2节点的数据;

LinkList.prototype.get = (position) => {//get(position) {
    //1.越界判断
    // 当position = length时,取到的是null所以0 =< position < length
    if(position < 0 || position >= this.length){
  		return null
    }
    //2.获取指定的positon位置的后一个节点的data
    //同样使用一个变量间接操作节点
    let current = this.head
    let index = 0
    while(index++ < position){
        current = current.next
    }
	return current.data
}

测试代码:

//1.创建LinkList
let list = new LinkList()
//2.插入数据
list.append('aaa')
list.append('bbb')
list.append('ccc')	
//3.测试get方法
console.log(list.get(0));//Node {data: 'aaa', next: Node}
console.log(list.get(1));//Node {data: 'bbb', next: Node}
6.indexOf根据元素值返回元素位置

主要的思路是从head依次往后查找(需要定义指针current),如果当前指针不为空,就拿传入的元素值和当前current指针的元素值比较,如果相等说明是我们要找的,直接return,不相等就要依次往后指,并且index要+1。(注意index = node数-1)

LinkList.prototype.indexOf = data => {//indexOf(data) {
	//1.定义变量
    let current = this.head
    let index = 0
    //2.开始查找:只要current不指向null就一直循环
    while(current){
    	if(current.data == data){
        return index
    	}
    	current = current.next
    	index += 1
    } 
    //3.遍历完链表没有找到,返回-1
    return -1
}

测试代码:

//1.创建LinkList
let list = new LinkList()   
//2.插入数据
list.append('aaa')
list.append('bbb')
list.append('ccc')	   
//3.测试indexOf方法
console.log(list.indexOf('aaa'));// 0
console.log(list.indexOf('ccc'));// 2
7.update更新某个位置的元素

(1)通过声明current指针,拿到这个位置的元素
(2)改变该元素内部的data的值,不需要改变前后指针(因为对象地址不会变)

LinkList.prototype.update = (position, newData) => {//update(position, newData) {
    //1.越界判断
    //因为被修改的节点不能为null,所以position不能等于length
    if(position < 0 || position >= this.length ) return false;
    //2.拿到这个位置节点
    let current = this.head;
    let index = 0;
    while(index++ < postion) {
        current = current.next;
    }
    //3.将position位置的后一个节点的data修改成newData
    current.data = newData;
    return current;
}

测试代码:

//1.创建LinkList
let list = new LinkList()
//2.插入数据
list.append('aaa')
list.append('bbb')
list.append('ccc')	    
//3.测试update方法
list.update(0, 'aa')
list.update(1, 'bb')
console.log(list.toString());// aa bb ccc
8.removeAt删除某个位置的节点

(1)如果删除的是第一个位置,那么head直接指向第二个节点,其他不用动(被删除的第一个节点由于没有指针指向它,会被垃圾回收机制回收)
在这里插入图片描述
(2)如果删除的是其他的位置,就要让被删除节点的上一个节点指向被删除节点的下一个节点
在这里插入图片描述

LinkList.prototype.removeAt = position => {//removeAt(position) {
    //1.越界判断
	if (position < 0 || position >= this.length) {//position不能为length
    	return null
    }
    //2.删除元素
    //情况1:position = 0时(删除第一个节点)
    let current = this.head
    if (position ==0 ) {
        //情况2:position > 0时
    	this.head = this.head.next
    }else{
        let index = 0
        let previous = null
        while (index++ < position) {
            previous = current
            current = current.next
        }
        //循环结束后,current指向position后一个节点,previous指向current前一个节点
        //再使前一个节点的next指向current的next即可
	    previous.next = current.next
    }
    //3,length-1
    this.length -= 1
    //返回被删除节点的data,为此current定义在最上面
	return current.data
}

测试代码:

//1.创建LinkList
let list = new LinkList()
//2.插入数据
list.append('aaa')
list.append('bbb')
list.append('ccc')  
//3.测试removeAt方法
console.log(list.removeAt(0));
console.log(list.removeAt(0));
console.log(list.toString());// ccc
9.实现remove方法,删除指定data的元素
LinkList.prototype.remove = (data) => {//remove(data) {
	//1.获取data在列表中的位置
    let position = this.indexOf(data)
    //2.根据位置信息,删除结点
    return this.removeAt(position)
}
10.实现isEmpty方法,判断链表是否为空
LinkList.prototype.isEmpty = () => {//isEmpty() {
	return this.length == 0
}
11.实现size方法,判断链表的长度
LinkList.prototype.size = () => {//size() {
	return this.length
}

测试代码:

//1.创建LinkList
let list = new LinkList()
//2.插入数据
list.append('aaa')
list.append('bbb')
list.append('ccc')  
//3.测试三个方法
list.remove('bbb')
console.log(list.toString()) // aaa ccc
console.log(list.isEmpty()) // false
console.log(list.size()) // 2
四、封装双向链表
1.什么是双向链表

双向链表: 既可以从头遍历到尾,又可以从尾遍历到头。也就是说链表连接的过程是双向的,它的实现原理是:一个节点既有向前连接的引用,也有一个向后连接的引用。

双向链表的缺点:

(1)每次在插入或删除某个节点时,都需要处理四个引用,而不是两个,实现起来会困难些;
(2)相对于单向链表,所占内存空间更大一些;
(3)但是,相对于双向链表的便利性而言,这些缺点微不足道。
也就是说,双向链表就是用空间换时间

在这里插入图片描述

  • 双向链表不仅有head指针指向第一个节点,而且有tail指针指向最后一个节点;
  • 每一个节点由三部分组成:item储存数据、prev指向前一个节点、next指向后一个节点点;
  • 双向链表的第一个节点的prev指向null;
  • 双向链表的最后一个节点的next指向null;
2.创建双向链表类

先创建双向链表类DoubleLinklist,并添加基本属性

function DoubleLinklist(){
	//封装内部类:节点类
    function Node(data){
        this.data = data
        this.prev = null
        this.next = null
    }
    //属性
    this.head = null
    this.tail ==null
    this.length = 0
}
3.append向尾部插入元素

双向链表向尾部插入元素和单向不一样,这里不需要再遍历元素,直接拿到tail操作就可以了。在插入的时候有两种情况:
(1)链表为空,添加的是第一个节点
我们只需要让head和tail都指向这个节点就可以了
在这里插入图片描述

(2)情况2:添加的不是第一个节点,如下图所示:只需要改变相关引用的指向即可。

  • 通过:newNode.prev = this.tail:建立指向1;
  • 通过:this.tail.next = newNode:建立指向2;
  • 通过:this.tail = newNode:建立指向3

要注意改变变量指向的顺序,最后修改tail指向,这样未修改前tail始终指向原链表的最后一个节点。
在这里插入图片描述
在这里插入图片描述

append(data) {//DoubleLinklist.prototype.append = data => {
    //1.先生成一个节点
    const newNode = new Node(data);
    //2.添加的是第一个节点,只需要让head和tail都指向新节点
    if(this.length == 0) {
        this.head = newNode;
        this.tail = newNode;
    } else {
        //3.添加的不是第一个节点,直接找到尾部(不用遍历)
        newNode.pre = this.tail; //先让新节点pre指向之前的尾部节点
        this.tail.next = newNode; //之前尾部节点next指向新节点
        this.tail = newNode; //tail指向新节点
        console.log(newNode.next); //最后一个指向null
    }
    //4.长度加一
    this.length++;
}

测试:

let list = new DoublyLinedList();
list.append('aaa');
list.append('bbb');
console.log(list); 
4.toString链表数据转换为字符串

这里有两种转字符串的方法,分别是顺序逆序
主要原理都是定义current变量记录当前指向的节点。
首先让current指向第一个(最后一个)节点,然后通过 current = current.next 依次向后(向前)遍历。在while循环(或for循环)中以(current)作为条件遍历链表,只要current != null就一直遍历,由此可获取链表所有节点的数据。
在这里插入图片描述

//1、toString()方法
toString() {
    return this.backwardString();
}
//2.forwardString()方法
forwardString() {
    let result = '';
    let current = this.head;
    //依次向前遍历,获取每一个节点
    while (current) {
        result += current.data + ' ';
        current = current.next;
    }
    return result;
}
//3.backwardString()方法
backwardString() {
    let result = '';
    let current = this.tail;
    //依次向后遍历,获取每一个节点
    for (let i = 0; i < this.length; i++) {
        result += current.data + ' ';
        current = current.pre;
    }
    return result;
}

测试:

const list = new DoublyLinedList();
list.append('aaa');
list.append('bbb');
list.append('ccc');
console.log(list.toString()); //ccc bbb aaa 
console.log(list.forwardString()); //aaa bbb ccc
console.log(list.backwardString()); //ccc bbb aaa
5.insert向任意位置插入节点
insert(position, data) {
	//1.越界判断
    if (position < 0 || position > this.length) return false
    //2.根据data创建新的节点
    let newNode = new Node(data)
    //3.插入新节点
    //原链表为空
    //情况1:插入的newNode是第一个节点
    if (this.length == 0) {
    	this.head = newNode
        this.tail = newNode
     //原链表不为空
     }else {
         //情况2:position == 0
         if (position == 0) {
            this.head.prev = newNode
            newNode.next = this.head
            this.head = newNode
          //情况3:position == this.length 
          } else if(position == this.length){
          	this.tail.next = newNode
            newNode.prev = this.tail
            this.tail = newNode
            //情况4:0 < position < this.length
          }else{
            let current = this.head
            let index = 0
            while(index++ < position){
              current = current.next
            }
            //修改pos位置前后节点变量的指向
            newNode.next = current
            newNode.prev = current.prev
            current.prev.next = newNode
            current.prev = newNode
	    }
    }
    //4.length+1
    this.length += 1
    return true//返回true表示插入成功
}

测试:

//1.创建双向链表
let list = new DoubleLinklist()
//2.测试insert方法
list.insert(0, '插入链表的第一个元素')
list.insert(0, '在链表首部插入元素')
list.insert(1, '在链表中间插入元素')
list.insert(3, '在链表尾部插入元素')
console.log(list);
alert(list)

在这里插入图片描述

6.get获取某个位置的元素值
get(position) {
    //1.越界判断
    if (position < 0 || position >= this.length) return false;//获取元素时position不能等于length
    //2.1如果该元素在前半部分,从head开始找
    if (position < this.length / 2) {
        let current = this.head;
        for(let i = 0; i < position; i++) {
            current = current.next;
        }
        return current.data;
    } else {
        //2.2如果该元素在后半部分,从tail倒着找
        let current = this.tail;
        let index = this.length - 1;
        while(index-- > position) {
            current = current.pre;
        }
        return current.data;
    }
}

测试:

let list = new DoublyLinedList();
list.append('aaa');
list.append('bbb');
list.append('ccc');
list.append('ddd');
console.log(list.get(2)); //ccc
console.log(list.get(3)); //ddd
7.indexOf根据某个元素值获取该节点位置

和单向链表一样,从头找,找不到就返回-1,找到了就对比并返回索引

indexOf(data) {
    let current = this.head;
    let index = 0;
    while(current) {
        if(current.data == data) {
            return index;
        }else {
            current = current.next;
            index++;
        }
    }
    return -1
}

测试:

const list = new DoublyLinedList();
list.append('aaa');
list.append('bbb');
list.append('ccc');
console.log(list.indexOf('bbb'));//1
8.update更新某个元素

主要思路还是先查找,然后把数据改了就行。查找的时候采用二分查找。

update(position, data) {
    //1.生成节点
    let newNode = new Node(data);
    //2.越界判断
    if(position < 0 || position >= this.length) return false;
    //3.寻找位置,改变元素值,二分查找
    let current = null;
    if(position < this.length / 2) {
        current = this.head;
        let index = 0;
        while(index++ < position) {
            current = current.next;
        }
    }else {
        current = this.tail;
        for(let i = this.length-1; i > position; i--) {
           current = current.pre; 
        }
    }
    current.data = data;
    return current.data;
}

测试:

let list = new DoublyLinedList();
list.append('aaa');
list.append('bbb');
list.append('ccc');
list.update(2,'ddd');
console.log(list.toString()); //aaa bbb ddd
9.removeAt删除某个位置的节点

(1)首先判断是否只有一个节点,如果只有一个节点,删除后head和tail都要置空.
(2)如果有多个节点,要看删除的是第一个、最后一个、其他位置。
(3)第一个位置,先清空第二节节点指向被删除元素的pre指针(清空所有指向它的指针,这样该节点在内存中没用途,会被自动回收),然后直接让head指向第二个节点就行了。
(4)最后一个位置,同理,删除倒数第二个节点指向被删除节点的next,然后让tail直接指向倒数第二个节点。
(5)删除完长度-1

removeAt(position) {
    //1.越界判断
    if(position < 0 || position >= this.length) return false;
    let current = this.head; //定义在最上面方便各种情况返回数据
    //2.判断是否只有一个节点
    if(this.length == 1) {
        //2.1如果只有一个节点,那么删除时head和tail都为空
        this.head = null;
        this.tail = null;
    }else {
        //2.2如果有多个节点,那么就要进行位置判断
        //2.2.1删除第一个节点
        if(position == 0) {
            //删除前head指向的是第一个节点
            this.head.next.pre = null; //所有指向它的指针都清掉
            this.head = this.head.next;
        }
        //2.2.2删除最后一个节点
        else if(position == this.length-1) {
            //删除前tail指向的是最后一个节点
            current = this.tail;
            this.tail.pre.next = null; //所有指向它的指针都清掉
            this.tail = this.tail.pre;
        }
        //2.2.3删除其他的节点
        else {
            //先找到位置
            let index = 0;
            while(index++ < position) {
                current = current.next;
            }
            current.pre.next = current.next;
            current.next.pre = current.pre;
        }
    }
    //3.删除完-1
    this.length -= 1;
    return current.data;
}

测试:

let list = new DoublyLinedList();
list.append('aaa');
list.append('bbb');
list.append('ccc');
list.removeAt(2);
console.log(list.toString()); //aaa bbb
9.remove方法,删除某个元素
remove(data) {
    return this.removeAt(this.indexOf(data));
}
10.isEmpty方法,测试是否为空
isEmpty() {
    return this.length == 0;
}
11.size方法,输出长度
size() {
    return this.length;
}
12.getHead方法,获取链表第一个元素值
getHead() {
    return this.head.data;
}
13.getTail方法,获取链表最后一个元素值
getTail() {
    return this.tail.data;
}

测试:

let list = new DoublyLinedList();
list.append('aaa');
list.append('bbb');
list.append('ccc');
list.append('ddd');
list.remove('bbb');
console.log(list.toString()); //aaa ccc
console.log(list.isEmpty());// false
console.log(list.size());// 3
console.log(list.getHead());// aaa
console.log(list.getTail());// ddd

更多参考:大佬

  • 2
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值