javascript数据结构与算法--链表

链表与数组的区别?

 1. 定义:

数组又叫做顺序表,顺序表是在内存中开辟一段连续的空间来存储数据,数组可以处理一组数据类型相同的数据,但不允许动态定义数组的大小,即在使用数组之前必须确定数组的大小。而在实际应用中,用户使用数组之前有时无法准确确定数组的大小,只能将数组定义成足够大小,这样数组中有些空间可能不被使用,从而造成内存空间的浪费。

链表是一种常见的数据组织形式,它采用动态分配内存的形式实现。链表是靠指针来连接多块不连续的的空间,在逻辑上形成一片连续的空间来存储数据。需要时可以用new分配内存空间,不需要时用delete将已分配的空间释放,不会造成内存空间的浪费。

2. 二者区分

A 从逻辑结构来看

  1. 数组必须事先定义固定的长度,不能适应数据动态地增减情况。当数据增加时,可能超出数组原先定义的数组的长度;当数据减少时,浪费内存。
  2. 链表可以动态地进行存储分配;可以适应数据动态增减情况;

B 从内存存储来看

  1. 数组是从栈中分配空间,对于程序员方便快速,自由度小。
  2. 链表是从堆中分配空间,自由度大但是申请管理比较麻烦。

C 从访问顺序来看

数组中的数据是按顺序来存储的,而链表是随机存储的。

  1. 要访问数组的元素需要按照索引来访问,速度比较快,如果对他进行插入删除操作的话,就得移动很多元素,所以对数组进行插入操作效率低。
  2. 由于链表是随机存储的,链表在插入,删除操作上有很高的效率(相对数组),如果要访问链表中的某个元素的话,那就得从链表的头逐个遍历,直到找到所需要的元素为止,所以链表的随机访问的效率比数组要低。

注意: 以上的区分在其他的编程语言 “数组和链表”的区别或许确实是这样的,但是在javascript的数组中并不存在上面的问题,因为 javascript有push,pop,shift,unshift,split等方法,所以不需要再访问数组中其他的元素了。

Javascript中数组的主要问题是:它们被实现成了对象,与其他语言(比如c++和java)的数组相比,效率很低。如果发现使用数组很慢的话,可以使用链表来替代它。至于在javascript中,一般情况下还是使用数组比较方便,我个人建议使用数组,但是现在我们还是要介绍下链表的基本概念,至少我们有一个理念,什么是链表,这个我们应该要知道的。所以下面我们来慢慢来分析链表的基本原理了。

一:定义链表

链表是由一组节点组成的集合。每个节点都使用一个对象的引用指向它的后继。指向另一个节点的引用叫做链。如下图一:

数组元素靠他们位置的索引进行引用,而链表元素则是靠相互之间的关系进行引用。如上图一:我们说B跟在A的后面,而不是和数组一样说B是链表中的第二个元素。遍历链表,就是跟着链接,从链表的首元素一直遍历到尾元素(不包含链表的头节点,头节点一般用来作为链表的接入点)。而链表的尾元素指向null 如上图1.

二:单向链表插入新节点和删除一个节点的原理;

1. 单向链表插入新节点,如上图2所示;链表中插入一个节点效率很高。向链表中插入一个节点,需要修改它前面的节点(前驱),使其指向新加入的节点,而新加入的节点则指向原来前驱指向的节点。

2. 单向链表删除一个节点;如上图3所示;从链表中删除一个元素也很简单,将待删除元素的前驱节点指向待删除元素的后继节点,同时将待删除元素指向null,元素就删除成功了。

三:设计一个基于对象的链表。

1 先设计一个创建节点Node类;如下:

function Node(element) {
    this.element = element;
    this.next = null;
}

Node类包含2个属性,element用来保存节点上的数据,next用来保存指向下一个节点的链接(指针)。

2. 再设计一个对链表进行操作的方法,包括插入删除节点,在列表中查找给定的值等。

function LinkTable () {
      this.head = new Node(“head”);
}

上面的链表类只有一个属性,那就是使用一个Node对象来保存该链表的头节点。

一:插入新节点insert方法步骤如下;

  1. 需要明确知道新节点要在那个节点前面或者后面插入。
  2. 在一个已知节点后面插入元素时,先要找到后面的节点。

因此在创建新节点之前,先要创建查找节点的方法 find,如下:

function find(item){
   var curNode = this.head;
   while(curNode.element != item) {
        curNode = curNode.next;
   }
  return curNode;
}

如上代码的意思:首先创建一个新节点,并将链表的头节点赋给这个新创建的节点curNode,然后再链表上进行循环,如果当前节点的element属性和我们要找的信息不符合,就从当前的节点移动到下一个节点,如果查找成功,该方法返回包含该数据的节点,否则的话 返回null。

一旦找到 “后面”的节点了,就可以将新节点插入到链表中了。首先将新节点的next属性设置为 “后面”节点的next属性对应的值。然后设置 “后面”节点的next属性指向新节点。Insert方法定义如下:

function insert(newElement,item) {
      var newNode = new Node(newElement);
      var current = this.find(item);
      newNode.next = current.next;
      newNode.previous = current;

      current.next = newNode;

}

二:定义一个显示链表中的元素。

function display (){
      var curNode = this.head;
      while(!(curNode.next == null)) {
             console.log(curNode.next.element);
             curNode = curNode.next;
      }
}

该方法先将列表的头节点赋给一个变量curNode,然后循环遍历列表,如果当前节点的next属性为null时,则循环结束。

下面是添加节点的所有JS代码;如下:

function Node(element) {

       this.element = element;

       this.next = null;

  }

  function LinkTable() {

       this.head = new Node("head");

}

LinkTable.prototype = {
    find: function(item){
        var curNode = this.head;
        while(curNode.element != item) {
             curNode = curNode.next;
        }
        return curNode;
    },

    insert: function(newElement,item) {
        var newNode = new Node(newElement);
        var current = this.find(item);
        newNode.next = current.next;
        current.next = newNode;
    },
    display: function(){
        var curNode = this.head;
        while(!(curNode.next == null)) {
           console.log(curNode.next.element);
           curNode = curNode.next;
        }
     }
  }

我们可以先来测试如上面的代码;

如下初始化;

var test = new LinkTable();

test.insert("a","head");

test.insert("b","a");

test.insert("c","b");

test.display();

1 执行test.insert("a","head"); 意思是说把a节点插入到头节点 head的后面去,执行到上面的insert方法内中的代码 var current = this.find(item); item就是头节点head传进来的;那么变量current值是 截图如下:

继续走到下面 newNode.next = current.next; 给新节点newNode的next属性指向null,继续走,current.next = newNode; 设置后面的节点next属性指向新节点a;如下:

2. 同上面原理一样,test.insert("b","a"); 我们接着走 var current = this.find(item);

那么现在的变量current值是如下:

继续走 newNode.next = current.next; 给新节点newNode的next属性指向null,继续走,current.next = newNode; 设置后面的节点next属性指向新节点b;如下:

test.insert("c","b"); 在插入一个c 原理也和上面执行一样,所以不再一步一步讲了,所以最后执行 test.display();方法后,将会打印出a,b,c

三:从链表中删除一个节点;

原理是:从链表中删除节点时,需要先找到待删除节点前面的节点。找到这个节点后,修改它的next属性使其不再指向待删除的节点,而是指向待删除节点的下一个节点。如上面的图三所示:

现在我们可以定义一个方法 findPrevious()。该方法遍历链表中的元素,检查每一个节点的下一个节点中是否存储着待删除数据,如果找到的话,返回该节点,这样就可以修改它的next属性了。如下代码:

function findPrevious (item) {
   var curNode = this.head;
   while(!(curNode.next == null) && (curNode.next.element != item)) {
       curNode = curNode.next;
   }
   return curNode;
}

现在我们可以编写singleRemove方法了,如下代码:

function singleRemove(item) {
   var prevNode = this.findPrevious(item);
   if(!(prevNode.next == null)) {
       prevNode.next = prevNode.next.next;
   }
}

下面所有的JS代码如下:

function Node(element) {
    this.element = element;
    this.next = null;
}
function LinkTable() {
    this.head = new Node("head");
}
LinkTable.prototype = {

    find: function(item){
        var curNode = this.head;
        while(curNode.element != item) {
            curNode = curNode.next;
        }
        return curNode;
    },
    insert: function(newElement,item) {
        var newNode = new Node(newElement);
        var current = this.find(item);
        newNode.next = current.next;
        current.next = newNode;
    },
    display: function(){
        var curNode = this.head;
        while(!(curNode.next == null)) {
            console.log(curNode.next.element);
            curNode = curNode.next;
        }
    },
    findPrevious: function(item) {
        var curNode = this.head;
        while(!(curNode.next == null) && (curNode.next.element != item)) {
            curNode = curNode.next;
        }
       return curNode;
    },
    singleRemove: function(item) {
        var prevNode = this.findPrevious(item);
        if(!(prevNode.next == null)) {
            prevNode.next = prevNode.next.next;
        }
    }
}

下面我们再来测试下代码,如下测试;

var test = new LinkTable();
test.insert("a","head");
test.insert("b","a");
test.insert("c","b");
test.display();

test.singleRemove("a");
test.display();

当执行 test.singleRemove("a");  删除链表a时,执行到singleRemove方法内的var prevNode = this.findPrevious(item); 先找到前面的节点head,如下:

然后在singleRemove方法内判断上一个节点head是否有下一个节点,如上所示,很明显有下一个节点,那么就把当前节点的下一个节点 指向 当前的下一个下一个节点,那么当前的下一个节点就被删除了,如下所示:

二:双向链表

双向链表图,如下图一所示:

前面我们介绍了是单向链表,在Node类里面定义了2个属性,一个是element是保存新节点的数据,还有一个是next属性,该属性指向后驱节点的链接,那么现在我们需要反过来,所以我们需要一个指向前驱节点的链接,我们现在把他叫做previous。Node类代码现在改成如下:

function Node(element) {
    this.element = element;
    this.next = null;
    this.previous = null;
}

1. 那么双向链表中的insert()方法和单向链表的方法类似,但是需要设置新节点previous属性,使其指向该节点的前驱。代码如下:

function insert(newElement,item) {
    var newNode = new Node(newElement);
    var current = this.find(item);
    newNode.next = current.next;
    newNode.previous = current;
    current.next = newNode;
}

2. 双向链表的doubleRemove() 删除节点方法比单向链表的效率更高,因为不需要再查找前驱节点了。那么双向链表的删除原理如下:

1. 首先需要在链表中找出存储待删除数据的节点,然后设置该节点前驱的next属性,使其指向待删除节点的后继。

2. 设置该节点后继的previous属性,使其指向待删除节点的前驱。

如上图2所示;首先在链表中找到删除节点C,然后设置该C节点前驱的B的next属性,那么B指向尾节点Null了;设置该C节点后继的(也就是尾部节点Null)的previous属性,使尾部Null节点指向待删除C节点的前驱,也就是指向B节点,即可把C节点删除掉。

代码可以如下:

function doubleRemove(item) {
   var curNode = this.find(item);
   if(!(curNode.next == null)) {
       curNode.previous.next = curNode.next;
       curNode.next.previous = curNode.previous;
       curNode.next = null;
       curNode.previous = null;
   }
}

比如测试代码如下:

var test = new LinkTable();

test.insert("a","head");

test.insert("b","a");

test.insert("c","b");

test.display();  // 打印出a,b,c

console.log("------------------");

test.doubleRemove("b");  // 删除b节点

test.display();  // 打印出a,c

console.log("------------------");

进入doubleRemove方法,先找到待删除的节点,如下:

然后设置该节点前驱的next属性 ,使其指向待删除节点的后继,如上代码

curNode.previous.next = curNode.next;

该节点的后继的previous属性,使其指向待删除节点的前驱。如上代码

curNode.next.previous = curNode.previous;

再设置 curNode.next = null; 再查看curNode值如下图所示:

下一个节点为null,同理当设置完 curNode.previous = null 的时候,会打印出如下:

我们可以再看看如上图2 删除节点的图 就可以看到,C节点与它的前驱节点B,与它的尾节点都断开了。即都指向null。

注意:双向链表中删除节点貌似不能删除最后一个节点,比如上面的C节点,为什么呢?因为当执行到如下代码时,就不执行了,如下图所示;

上面的是删除节点的分析,现在我们再来分析下 双向链表中的 添加节点的方法insert(); 我们再来看下;

当执行到代码 test.insert("a","head"); 把a节点插入到头部节点后面去,我们来看看insert方法内的这一句代码;

newNode.previous = current; 如下所示;

当执行到如下这句代码时候;

current.next = newNode;

如下所示;

同理插入b节点,c节点也类似的原理。

双向链表反序操作;现在我们也可以对双向链表进行反序操作,现在需要给双向链表增加一个方法,用来查找最后的节点。如下代码;

function findLast(){
    var curNode = this.head;
    while(!(curNode.next == null)) {
        curNode = curNode.next;
    }
    return curNode;
}

如上findLast()方法就可以找到最后一个链表中最后一个元素了。现在我们可以写一个反序操作的方法了,如下:

function dispReverse(){
    var curNode = this.head;
    curNode = this.findLast();
    while(!(curNode.previous == null)) {
        console.log(curNode.element);
        curNode = curNode.previous;
    }
}

如上代码先找到最后一个元素,比如C,然后判断当前节点的previous属性是否为空,截图如下;

可以看到当前节点的previous不为null,那么执行到console.log(curNode.element); 先打印出c,然后把当前的curNode.previous 指向与curNode了(也就是现在的curNode是b节点),如下所示;

同理可知;所以分别打印出c,b,a了。

时间: 2024-10-11 01:15:45

javascript数据结构与算法--链表的相关文章

重读《学习JavaScript数据结构与算法-第三版》- 第6章 链表(一)

定场诗 伤情最是晚凉天,憔悴厮人不堪言: 邀酒摧肠三杯醉.寻香惊梦五更寒. 钗头凤斜卿有泪,荼蘼花了我无缘: 小楼寂寞新雨月.也难如钩也难圆. 前言 本章为重读<学习JavaScript数据结构与算法>的系列文章,该章节主要讲述数据结构-链表,以及实现链表的过程和原理. 链表 链表,为什么要有这种数据结构呢?当然,事出必有因! 数组-最常用.最方便的数据结构,But,当我们从数组的起点或中间插入或移动项的成本很高,因为我们需要移动数组元素. 链表,是存储有序的元素集合.链表中的元素在内存中并不

javascript数据结构和算法 零(前记+序言)

前记 这本书Data Structure and Algorithm with Javascript 我将其翻译成<< javascript 数据结构和算法>> 为什么这么翻译?而不是翻译成"用javascript实现数据结构和算法" 原因有二: 一个,我觉的它更加简洁,第二个,我是想强调我们在这里使用的是javascript. 我不喜欢一些翻译的人把一个简单的语句进行加工,搞得晦涩难懂.我更喜欢外国人写书的 那种原汁原味.同样,我很喜欢外国人的那种分享精神,它

javascript数据结构和算法 一(本书组织结构)

环境 我们在本书中使用的Javascript编程环境是基于SpiderMonkey 引擎的javascript shell. 第一章讲述了在你的环境中下载 shell的介绍. 其他的shell例如Nodejs同样可以工作,虽然你可能需要简单的改变下代码. 除了shell,唯一你要准备的就是文本编辑器. 本书的组织结构 第一章:javascript语言的概览,至少是在本书中使用的javascript的一些特征.这一章同时展示了在整本书中使用的一些编程风格. 第二章:讨论了在计算机科学中最通用的数据

重读《学习JavaScript数据结构与算法-第三版》- 第3章 数组(一)

定场诗 大将生来胆气豪,腰横秋水雁翎刀. 风吹鼍鼓山河动,电闪旌旗日月高. 天上麒麟原有种,穴中蝼蚁岂能逃. 太平待诏归来日,朕与先生解战袍. 此处应该有掌声... 前言 读<学习JavaScript数据结构与算法>- 第3章 数组,本节将为各位小伙伴分享数组的相关知识:概念.创建方式.常见方法以及ES6数组的新功能. 数组 数组是最简单的内存数据结构,用于存储一系列同一种数据类型的值. 注:虽然数组支持存储不同类型的值,但建议遵守最佳实践. 一.数组基础 创建和初始化数组 new Array

算法入门《数据结构与算法图解》+《我的第一本算法书》+《学习JavaScript数据结构与算法第3版》

最近几年学前端的人会越来越多,再加上前端的范围越来越广,从前端发展为全栈,数据结构和算法的功底要求势必将越来越高. <数据结构与算法图解>电子书及代码是数据结构与算法的入门指南,不局限于某种特定语言,略过复杂的数学公式,用通俗易懂的方式针对编程初学者介绍数据结构与算法的基本概念,培养编程逻辑.主要内容包括:为什么要了解数据结构与算法,大O表示法及其代码优化利用,栈.队列等的合理使用,等等. <算法图解>电子书非常的体贴,看起来也很快,用图来解释算法是非常好的想法,可作为学习数据结构

javascript数据结构与算法--二叉树(插入节点、生成二叉树)

javascript数据结构与算法-- 插入节点.生成二叉树 二叉树中,相对较小的值保存在左节点上,较大的值保存在右节点中 /* *二叉树中,相对较小的值保存在左节点上,较大的值保存在右节点中 * * * */ /*用来生成一个节点*/ function Node(data, left, right) { this.data = data;//节点存储的数据 this.left = left; this.right = right; this.show = show; } function sh

javascript数据结构与算法---列表

前言:在日常生活中,人们经常要使用列表,比如我们有时候要去购物时,为了购物时东西要买全,我们可以在去之前,列下要买的东西,这就要用的列表了,或者我们小时候上学那段时间,每次考完试后,学校都会列出这次考试成绩前十名的同学的排名及成绩单,等等这些都是列表的列子.我们计算机内也在使用列表,那么列表适合使用在什么地方呢?不适合使用在什么地方呢? 适合使用在:当列表的元素不是很多的情况下,可以使用列表,因为对列表中的元素查找或者排序时,效率还算非常高,反之:如果列表元素非常多的情况下,就不适合使用列表了.

javascript数据结构与算法---队列

队列是一种列表,不同的是队列只能在队尾插入元素,在队首删除元素.队列用于存储按顺序排列的数据,先进先出,这点和栈不一样(后入先出).在栈中,最后入栈的元素反而被优先处理.我们现在可以把队列想象对我们去餐馆吃饭的情景,很多人排队吃饭,排在最前面的人先打饭.新来的人只能在后面排队.直到轮到他们为止. 一:对队列的操作 队列有2种主要的操作,向队尾中插入新元素enqueue()方法和删除队列中的队首的元素的dequeue()方法,另外我们还有一个读取队头的元素,这个方法我们可以叫front()方法.该

数据结构与算法-链表的基本操作---ShinPans

//链表操作:建立.插入.删除.查找.倒置.删除等基本操作 #include<stdio.h> #include<stdlib.h> typedef  struct LNode {       int data;       structLNode *next; }LNode,*Llist; LNode *creat_head();//创建一个空表 void creat_list(LNode *,int);//创建一个长度为n的线性链表 void insert_list(LNode