首发于 全栈修炼
JavaScript 数据结构与算法之美 - 线性表(数组、栈、队列、链表)

JavaScript 数据结构与算法之美 - 线性表(数组、栈、队列、链表)

前言

基础知识就像是一座大楼的地基,它决定了我们的技术高度。
我们应该多掌握一些可移值的技术或者再过十几年应该都不会过时的技术,数据结构与算法就是其中之一。

栈、队列、链表、堆 是数据结构与算法中的基础知识,是程序员的地基。

笔者写的 JavaScript 数据结构与算法之美 系列用的语言是 JavaScript ,旨在入门数据结构与算法和方便以后复习。

1. 线性表与非线性表

线性表(Linear List):就是数据排成像一条线一样的结构。每个线性表上的数据最多只有前和后两个方向。数组、链表、队列、栈 等就是线性表结构。

非线性表:数据之间并不是简单的前后关系。二叉树、堆、图 就是非线性表。

本文主要讲线性表,非线性表会在后面章节讲。

2. 数组

定义

特点

注意

但是因为 JavaScript 是弱类型的语言,弱类型则允许隐式类型转换。

隐式:是指源码中没有明显的类型转换代码。也就是说,一个变量,可以赋值字符串,也可以赋值数值。

你还可以直接让字符串类型的变量和数值类型的变量相加,虽然得出的最终结果未必是你想象的那样,但一定不会报错。

数组的每一项可以是不同的类型,比如:

定义的数组的大小是可变的,不像强类型语言,定义某个数组变量的时候就要定义该变量的大小。

实现

JavaScript 原生支持数组,而且提供了很多操作方法,这里不展开讲。

3. 栈

定义

  1. 后进者先出,先进者后出,简称 后进先出(LIFO),这就是典型的结构。
  2. 新添加的或待删除的元素都保存在栈的末尾,称作栈顶,另一端就叫栈底
  3. 在栈里,新元素都靠近栈顶,旧元素都接近栈底。
  4. 从栈的操作特性来看,是一种 操作受限的线性表,只允许在一端插入和删除数据。
  5. 不包含任何元素的栈称为空栈

栈也被用在编程语言的编译器和内存中保存变量、方法调用等,比如函数的调用栈。

实现

栈的方法:

测试:

栈的应用实例: JavaScript 数据结构与算法之美 - 实现一个前端路由,如何实现浏览器的前进与后退 ?

4. 队列

普通队列

定义

实现

队列里面有一些声明的辅助方法:

代码:

测试:

优先队列

定义

优先队列中元素的添加和移除是依赖优先级的。

应用

优先队列分为两类

最小优先队列是把优先级的值最小的元素被放置到队列的最前面(代表最高的优先级)。 比如:有四个元素:"John", "Jack", "Camila", "Tom",他们的优先级值分别为 4,3,2,1。 那么最小优先队列排序应该为:"Tom","Camila","Jack","John"。

最大优先队列正好相反,把优先级值最大的元素放置在队列的最前面。 以上面的为例,最大优先队列排序应该为:"John", "Jack", "Camila", "Tom"。

实现

实现一个优先队列,有两种选项:

  1. 设置优先级,根据优先级正确添加元素,然后和普通队列一样正常移除
  2. 设置优先级,和普通队列一样正常按顺序添加,然后根据优先级移除

这里最小优先队列和最大优先队列我都采用第一种方式实现,大家可以尝试一下第二种。

下面只重写 enqueue() 方法和 print() 方法,其他方法和上面的普通队列完全相同。

实现最小优先队列

实现最小优先队列 enqueue() 方法和 print() 方法:

最小优先队列测试:

实现最大优先队列

最大优先队列测试:

循环队列

定义

循环队列,顾名思义,它长得像一个环。把它想像成一个圆的钟就对了。

关键是:确定好队空和队满的判定条件。

循环队列的一个例子就是击鼓传花游戏(Hot Potato)。在这个游戏中,孩子们围城一个圆圈,击鼓的时候把花尽快的传递给旁边的人。某一时刻击鼓停止,这时花在谁的手里,谁就退出圆圈直到游戏结束。重复这个过程,直到只剩一个孩子(胜者)。

下面我们在普通队列的基础上,实现一个模拟的击鼓传花游戏,下面只写击鼓传花的代码片段:

执行结果为:

队列小结

一些具有某些额外特性的队列,比如:循环队列、阻塞队列、并发队列。它们在很多偏底层系统、框架、中间件的开发中,起着关键性的作用。

以上队列的代码要感谢 leocoder351

5. 链表

定义

简单的链接结构图:

其中,data 中保存着数据,next 保存着下一个链表的引用。 上图中,我们说 data2 跟在 data1 后面,而不是说 data2 是链表中的第二个元素。值得注意的是,我们将链表的尾元素指向了 null 节点,表示链接结束的位置。

特点

三种最常见的链表结构,它们分别是:

单链表

定义

由于链表的起始点的确定比较麻烦,因此很多链表的实现都会在链表的最前面添加一个特殊的节点,称为 头节点,表示链表的头部。

经过改造,链表就成了如下的样子:

针对链表的插入和删除操作,我们只需要考虑相邻结点的指针改变,所以插入与删除的时间复杂度为 O(1)。

在 d2 节点后面插入 d4 节点:

删除 d4 节点:

实现

单向链表的八种常用操作:

具体代码:

// 单链表
function SinglyLinkedList() {
	// 节点
	function Node(element) {
		this.element = element; // 当前节点的元素
		this.next = null; // 下一个节点指针
	}

	var length = 0; // 链表的长度
	var head = null; // 链表的头部节点

	// 向链表尾部添加一个新的节点
	this.append = function(element) {
		var node = new Node(element);
		var currentNode = head;

		// 判断是否为空链表
		if (head === null) {
			// 是空链表,就把当前节点作为头部节点
			head = node;
		} else {
			// 从 head 开始一直找到最后一个 node
			while (currentNode.next) {
				// 后面还有 node
				currentNode = currentNode.next;
			}
			// 把当前节点的 next 指针 指向 新的节点
			currentNode.next = node;
		}
		// 链表的长度加 1
		length++;
	};

	// 向链表特定位置插入一个新节点
	this.insert = function(position, element) {
		if (position < 0 && position > length) {
			// 越界
			return false;
		} else {
			var node = new Node(element);
			var index = 0;
			var currentNode = head;
			var previousNode;

			// 在最前插入节点
			if (position === 0) {
				node.next = currentNode;
				head = node;
			} else {
				// 循环找到位置
				while (index < position) {
					index++;
					previousNode = currentNode;
					currentNode = currentNode.next;
				}
				// 把前一个节点的指针指向新节点,新节点的指针指向当前节点,保持连接性
				previousNode.next = node;
				node.next = currentNode;
			}

			length++;

			return true;
		}
	};

	// 从链表的特定位置移除一项
	this.removeAt = function(position) {
		if ((position < 0 && position >= length) || length === 0) {
			// 越界
			return false;
		} else {
			var currentNode = head;
			var index = 0;
			var previousNode;

			if (position === 0) {
				head = currentNode.next;
			} else {
				// 循环找到位置
				while (index < position) {
					index++;
					previousNode = currentNode;
					currentNode = currentNode.next;
				}
				// 把当前节点的 next 指针 指向 当前节点的 next 指针,即是 删除了当前节点
				previousNode.next = currentNode.next;
			}

			length--;

			return true;
		}
	};

	// 从链表中移除指定项
	this.remove = function(element) {
		var index = this.indexOf(element);
		return this.removeAt(index);
	};

	// 返回元素在链表的索引,如果链表中没有该元素则返回 -1
	this.indexOf = function(element) {
		var currentNode = head;
		var index = 0;

		while (currentNode) {
			if (currentNode.element === element) {
				return index;
			}

			index++;
			currentNode = currentNode.next;
		}

		return -1;
	};

	// 如果链表中不包含任何元素,返回 true,如果链表长度大于 0,返回 false
	this.isEmpty = function() {
		return length === 0;
	};

	// 返回链表包含的元素个数,与数组的 length 属性类似
	this.size = function() {
		return length;
	};

	// 获取链表头部元素
	this.getHead = function() {
		return head.element;
	};

	// 由于链表使用了 Node 类,就需要重写继承自 JavaScript 对象默认的 toString() 方法,让其只输出元素的值
	this.toString = function() {
		var currentNode = head;
		var string = '';

		while (currentNode) {
			string += ',' + currentNode.element;
			currentNode = currentNode.next;
		}

		return string.slice(1);
	};

	// 打印链表数据
	this.print = function() {
		console.log(this.toString());
	};

	// 获取整个链表
	this.list = function() {
		console.log('head: ', head);
		return head;
	};
}

测试:

整个链表数据在 JavaScript 里是怎样的呢 ?

为了看这个数据,特意写了个 list 函数:

重点上上面的最后一行代码: singlyLinked.list() ,打印的数据如下:

所以,在 JavaScript 中,单链表的真实数据有点类似于对象,实际上是 Node 类生成的实例。

双向链表

单向链表只有一个方向,结点只有一个后继指针 next 指向后面的结点。 而双向链表,它支持两个方向,每个结点不止有一个后继指针 next 指向后面的结点,还有一个前驱指针 prev 指向前面的结点。

单向链表与又向链表比较

实现

具体代码:

// 创建双向链表 DoublyLinkedList 类
function DoublyLinkedList() {
  function Node(element) {
    this.element = element; //当前节点的元素
    this.next = null; //下一个节点指针
    this.previous = null; //上一个节点指针
  }

  var length = 0; // 链表长度
  var head = null; // 链表头部
  var tail = null; // 链表尾部

  // 向链表尾部添加一个新的项
  this.append = function(element) {
    var node = new Node(element);
    var currentNode = tail;

    // 判断是否为空链表
    if (currentNode === null) {
      // 空链表
      head = node;
      tail = node;
    } else {
      currentNode.next = node;
      node.prev = currentNode;
      tail = node;
    }

    length++;
  };

  // 向链表特定位置插入一个新的项
  this.insert = function(position, element) {
    if (position < 0 && position > length) {
      // 越界
      return false;
    } else {
      var node = new Node(element);
      var index = 0;
      var currentNode = head;
      var previousNode;

      if (position === 0) {
        if (!head) {
          head = node;
          tail = node;
        } else {
          node.next = currentNode;
          currentNode.prev = node;
          head = node;
        }
      } else if (position === length) {
        this.append(element);
      } else {
        while (index < position) {
          index++;
          previousNode = currentNode;
          currentNode = currentNode.next;
        }

        previousNode.next = node;
        node.next = currentNode;

        node.prev = previousNode;
        currentNode.prev = node;
      }

      length++;

      return true;
    }
  };

  // 从链表的特定位置移除一项
  this.removeAt = function(position) {
    if ((position < 0 && position >= length) || length === 0) {
      // 越界
      return false;
    } else {
      var currentNode = head;
      var index = 0;
      var previousNode;

      if (position === 0) {
        // 移除第一项
        if (length === 1) {
          head = null;
          tail = null;
        } else {
          head = currentNode.next;
          head.prev = null;
        }
      } else if (position === length - 1) {
        // 移除最后一项
        if (length === 1) {
          head = null;
          tail = null;
        } else {
          currentNode = tail;
          tail = currentNode.prev;
          tail.next = null;
        }
      } else {
        while (index < position) {
          index++;
          previousNode = currentNode;
          currentNode = currentNode.next;
        }
        previousNode.next = currentNode.next;
        previousNode = currentNode.next.prev;
      }

      length--;

      return true;
    }
  };

  // 从链表中移除指定项
  this.remove = function(element) {
    var index = this.indexOf(element);
    return this.removeAt(index);
  };

  // 返回元素在链表的索引,如果链表中没有该元素则返回 -1
  this.indexOf = function(element) {
    var currentNode = head;
    var index = 0;

    while (currentNode) {
      if (currentNode.element === element) {
        return index;
      }

      index++;
      currentNode = currentNode.next;
    }

    return -1;
  };

  // 如果链表中不包含任何元素,返回 true ,如果链表长度大于 0 ,返回 false
  this.isEmpty = function() {
    return length == 0;
  };

  // 返回链表包含的元素个数,与数组的 length 属性类似
  this.size = function() {
    return length;
  };

  // 获取链表头部元素
  this.getHead = function() {
    return head.element;
  };

  // 由于链表使用了 Node 类,就需要重写继承自 JavaScript 对象默认的 toString() 方法,让其只输出元素的值
  this.toString = function() {
    var currentNode = head;
    var string = '';

    while (currentNode) {
      string += ',' + currentNode.element;
      currentNode = currentNode.next;
    }

    return string.slice(1);
  };

  this.print = function() {
    console.log(this.toString());
  };

  // 获取整个链表
  this.list = function() {
    console.log('head: ', head);
    return head;
  };
}

测试:

整个链表数据在 JavaScript 里是怎样的呢 ?

调用 doublyLinked.list(); .

控制台输出如下:

链表代码实现的关键是弄清楚:前节点与后节点与边界。

循环链表

循环链表是一种特殊的单链表。 循环链表和单链表相似,节点类型都是一样。 唯一的区别是,在创建循环链表的时候,让其头节点的 next 属性指向它本身。 即:

这种行为会导致链表中每个节点的 next 属性都指向链表的头节点,换句话说,也就是链表的尾节点指向了头节点,形成了一个循环链表。如下图所示:

循环链表:在单链表的基础上,将尾节点的指针指向头结点,就构成了一个循环链表。环形链表从任意一个节点开始,都可以遍历整个链表。

代码:

// 循环链表
function CircularLinkedList() {
	// 节点
	function Node(element) {
		this.element = element; // 当前节点的元素
		this.next = null; // 下一个节点指针
	}

	var length = 0,
		head = null;

	this.append = function(element) {
		var node = new Node(element),
			current;

		if (!head) {
			head = node;
			// 头的指针指向自己
			node.next = head;
		} else {
			current = head;

			while (current.next !== head) {
				current = current.next;
			}

			current.next = node;
			// 最后一个节点指向头节点
			node.next = head;
		}

		length++;
		return true;
	};

	this.insert = function(position, element) {
		if (position > -1 && position < length) {
			var node = new Node(element),
				index = 0,
				current = head,
				previous;

			if (position === 0) {
				// 头节点指向自己
				node.next = head;
				head = node;
			} else {
				while (index++ < position) {
					previous = current;
					current = current.next;
				}
				previous.next = node;
				node.next = current;
			}
			length++;
			return true;
		} else {
			return false;
		}
	};
	this.removeAt = function(position) {
		if (position > -1 && position < length) {
			var current = head,
				previous,
				index = 0;
			if (position === 0) {
				head = current.next;
			} else {
				while (index++ < position) {
					previous = current;
					current = current.next;
				}
				previous.next = current.next;
			}
			length--;
			return current.element;
		} else {
			return false;
		}
	};
	this.remove = function(element) {
		var current = head,
			previous,
			indexCheck = 0;
		while (current && indexCheck < length) {
			if (current.element === element) {
				if (indexCheck == 0) {
					head = current.next;
					length--;
					return true;
				} else {
					previous.next = current.next;
					length--;
					return true;
				}
			} else {
				previous = current;
				current = current.next;
				indexCheck++;
			}
		}
		return false;
	};
	this.remove = function() {
		if (length === 0) {
			return false;
		}
		var current = head,
			previous,
			indexCheck = 0;
		if (length === 1) {
			head = null;
			length--;
			return current.element;
		}
		while (indexCheck++ < length) {
			previous = current;
			current = current.next;
		}
		previous.next = head;
		length--;
		return current.element;
	};
	this.indexOf = function(element) {
		var current = head,
			index = 0;
		while (current && index < length) {
			if (current.element === element) {
				return index;
			} else {
				index++;
				current = current.next;
			}
		}
		return -1;
	};
	this.isEmpty = function() {
		return length === 0;
	};
	this.size = function() {
		return length;
	};

	// 由于链表使用了 Node 类,就需要重写继承自 JavaScript 对象默认的 toString() 方法,让其只输出元素的值
	this.toString = function() {
		var current = head,
			string = '',
			indexCheck = 0;
		while (current && indexCheck < length) {
			string += ',' + current.element;
			current = current.next;
			indexCheck++;
		}
		return string.slice(1);
	};

	// 获取链表头部元素
	this.getHead = function() {
		return head.element;
	};

	// 打印链表数据
	this.print = function() {
		console.log(this.toString());
	};

	// 获取整个链表
	this.list = function() {
		console.log('head: ', head);
		return head;
	};
}

测试:

整个链表数据在 JavaScript 里是怎样的呢 ?

调用 circularLinked.list() 。

控制台输出如下:

你知道大家发现没有,为什么从 1 - 4 - 1 了,还有 next 节点,而且是还可以一直点 next ,重复的展开下去,这正是 循环 的原因。

链表总结

6. 文章输出计划

JavaScript 数据结构与算法之美 的系列文章,坚持 3 - 7 天左右更新一篇,暂定计划如下表。

标题链接 时间和空间复杂度 github.com/biaochenxuyi

线性表(数组、链表、栈、队列) github.com/biaochenxuyi

实现一个前端路由,如何实现浏览器的前进与后退 ? github.com/biaochenxuyi

栈内存与堆内存 、浅拷贝与深拷贝 github.com/biaochenxuyi

非线性表(树、堆)

递归

冒泡排序

插入排序

选择排序

归并排序

快速排序

计数排序

基数排序

桶排序

希尔排序

堆排序

十大经典排序汇总

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。

7. 最后

关注我的公众号,第一时间接收最新的精彩博文。

文章可以转载,但须注明作者及出处,需要转载到公众号的,喊我加下白名单就行了。

如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

参考文章:

数组:为什么很多编程语言中数组都从 0 开始编号?

JS中的算法与数据结构——链表(Linked-list)

JavaScript数据结构 03 - 队列 链表(上):如何实现 LRU 缓存淘汰算法?

JavaScript数据结构——队列

深圳SEO优化公司娄底建设网站价格衡阳营销型网站建设公司潮州模板推广报价益阳百度网站优化推荐仙桃网络广告推广哪家好岳阳建站木棉湾网站排名优化报价昌吉企业网站建设多少钱石家庄网页制作推荐龙岗网页制作推荐南山企业网站制作推荐昆明企业网站设计孝感百度网站优化排名宝鸡seo优化推荐伊犁网站改版公司运城网站推广方案推荐广元营销网站哪家好梅州高端网站设计哪家好霍邱网站优化按天扣费报价舟山企业网站设计推荐梧州网站优化按天计费仙桃网站关键词优化价格天津网站制作设计哪家好铜仁百姓网标王公司呼和浩特关键词按天计费报价娄底SEO按天计费价格大芬网站seo优化价格运城网页设计公司安阳网站关键词优化报价宜春至尊标王推荐歼20紧急升空逼退外机英媒称团队夜以继日筹划王妃复出草木蔓发 春山在望成都发生巨响 当地回应60岁老人炒菠菜未焯水致肾病恶化男子涉嫌走私被判11年却一天牢没坐劳斯莱斯右转逼停直行车网传落水者说“没让你救”系谣言广东通报13岁男孩性侵女童不予立案贵州小伙回应在美国卖三蹦子火了淀粉肠小王子日销售额涨超10倍有个姐真把千机伞做出来了近3万元金手镯仅含足金十克呼北高速交通事故已致14人死亡杨洋拄拐现身医院国产伟哥去年销售近13亿男子给前妻转账 现任妻子起诉要回新基金只募集到26元还是员工自购男孩疑遭霸凌 家长讨说法被踢出群充个话费竟沦为间接洗钱工具新的一天从800个哈欠开始单亲妈妈陷入热恋 14岁儿子报警#春分立蛋大挑战#中国投资客涌入日本东京买房两大学生合买彩票中奖一人不认账新加坡主帅:唯一目标击败中国队月嫂回应掌掴婴儿是在赶虫子19岁小伙救下5人后溺亡 多方发声清明节放假3天调休1天张家界的山上“长”满了韩国人?开封王婆为何火了主播靠辱骂母亲走红被批捕封号代拍被何赛飞拿着魔杖追着打阿根廷将发行1万与2万面值的纸币库克现身上海为江西彩礼“减负”的“试婚人”因自嘲式简历走红的教授更新简介殡仪馆花卉高于市场价3倍还重复用网友称在豆瓣酱里吃出老鼠头315晚会后胖东来又人满为患了网友建议重庆地铁不准乘客携带菜筐特朗普谈“凯特王妃P图照”罗斯否认插足凯特王妃婚姻青海通报栏杆断裂小学生跌落住进ICU恒大被罚41.75亿到底怎么缴湖南一县政协主席疑涉刑案被控制茶百道就改标签日期致歉王树国3次鞠躬告别西交大师生张立群任西安交通大学校长杨倩无缘巴黎奥运

深圳SEO优化公司 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化