常见数据结构特性

55 阅读12分钟

数组(Array)

概念

数组(Array)是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据。

特性

优势

  • 数组支持随机访问,根据下标随机访问的时间复杂度为O(1)
  • 适合查找操作,但是查找的时间复杂度并不是O(1),即便是排好序的数组,使用二分查找,时间复杂度也是O(logn)

劣势

  • 数组为了保持内存的数据的连续性,会导致插入、删除这两个操作比较低效。
    • 插入
      • 数组头插入:最坏情况时间复杂度O(n)
      • 数组中间插入:最坏情况时间复杂度O(n)
      • 尾部插入:最坏情况时间复杂度O(1),尾部插入不需要移动其他元素
    • 删除
      • 删除头部:最坏情况时间复杂度O(n)
      • 删除中间:最坏情况时间复杂度O(n)
      • 删除尾部:最坏情况时间复杂度O(1)

链表(LinkedList)

概念

链表是一种 物理存储结构上非连续 、非顺序的存储结构,数据元素的 逻辑顺序 是通过链表中的 指针链  次序实现的。

特性

优点

  • 由于链表上的元素在空间存储上内存地址不连续,所以随机增删元素的时候不会有大量元素位移,因此随机增删效率较高。

缺点

  • 不能通过数学表达式计算被查找元素的内存地址,每一次查找都是从头节点开始遍历,直到找到为止。所以LinkedList集合检索(查找)的效率较低,通常来说时间复杂度为O(n)

单链表和双链表的区别

单链表 (SLL)双链表 (DLL)
SLL 节点包含 2 个字段 – 数据字段和下一个链接字段。DLL 节点包含 3 个字段 – 数据字段、前一个链接字段和一个下一个链接字段。
在 SLL 中,只能使用下一个节点链接来完成遍历。因此,只能在一个方向上遍历。在 DLL 中,可以使用前一个节点链接或下一个节点链接来完成遍历。因此,可以在两个方向(向前和向后)进行遍历。
SLL 占用的内存比 DLL 少,因为它只有 2 个字段。DLL 比 SLL 占用更多的内存,因为它有 3 个字段。
在给定位置插入和删除的复杂度是 O(n)。在给定位置插入和删除的复杂性是 O(n / 2) = O(n),因为可以从头开始或从尾端进行遍历。
给定节点的删除复杂度为 O(n),因为需要知道前一个节点,遍历需要 O(n)给定节点的删除复杂度为 O(1),因为可以轻松访问前一个节点。
最喜欢使用单链表来执行堆栈。可以使用双向链表来执行堆和栈,二叉树。
当不需要执行任何搜索操作并且想要节省内存时,更喜欢单链表。为了更好的实现,在搜索时,更喜欢使用双向链表。
与双链表相比,单链表消耗的内存更少。与单链表相比,双向链表消耗更多的内存。

实现链表

实现单向链表

// 存储节点
function Node(element) {
	this.element = element
	this.next = null
}

function LinkedList(item) {
	this.head = new Node('head')  // 头节点

	LinkedList.prototype = {
		// 查找某一节点
		find: function (item) {
			var currentNode = this.head
			while (currentNode.element !== item) {
				currentNode = currentNode.next
			}
			return currentNode
		},
		// 往某一节点后面插入新节点
		insert: function (newItem, item) {
			var newNode = new Node(newItem)
			var currentNode = this.find(item)
			newNode.next = currentNode.next
			currentNode.next = newNode  // 这句很重要,是整个链表连接起来的关键
		},
		// 查找某一节点的前节点(前驱)
		findPrevious: function (item) {
			var currentNode = this.head
			while (!(currentNode.next === null) && currentNode.next.element !== item) {
				currentNode = currentNode.next
			}
			return currentNode
		},
		// 删除某一节点
		remove: function (item) {
			var previousNode = this.findPrevious(item)
			if (previousNode.next !== null) {
				previousNode.next = previousNode.next.next
			}
		},
		// 修改某一节点的数据
		edit: function (item, newItem) {
			var currentNode = this.find(item)
			currentNode.element = newItem
		},
		// 打印所有节点
		display: function () {
			var currentNode = this.head
			while (currentNode.next !== null) {
				console.log(currentNode.next.element)
				currentNode = currentNode.next
			}
		}
	}

}

实现双向链表


// 封装双向链表类
function DoubleLinkedList() {
	// 封装内部类:节点类
	function Node(data) {
		this.data = data
		this.pre = null
		this.next = null
	}

	// 属性
	this.head = null
	this.tail = null
	this.length = 0

	// 常见的方法
	// 1.append方法
	DoubleLinkedList.prototype.append = data => {
		//1. 根据data创建新节点
		let newNode = new Node(data)

		// 2.添加节点
		// 情况1:添加的是第一个节点
		if (this.length == 0) {
			this.head = newNode
			this.tail = newNode
		} else { // 情况2:添加的不是第一个节点
			newNode.pre = this.tail
			this.tail.next = newNode
			this.tail = newNode
		}

		// 3.length+1
		this.length += 1
	}

	// 2.将链表转变为字符串形式
	// 2.1 toString方法
	DoubleLinkedList.prototype.toString = () => {
		return this.backwardString()
	}
	// 2.2 forwardString方法
	DoubleLinkedList.prototype.forwardString = () => {
		// 1.定义变量
		let current = this.tail
		let resStr = ''

		// 2.依次向前遍历,获取每一个节点
		while (current) {
			resStr += current.data + '**'
			current = current.pre
		}
		return resStr
	}
	// 2.3 backwardString方法
	DoubleLinkedList.prototype.backwardString = () => {
		// 1.定义变量
		let current = this.head
		let resStr = ''

		// 2.依次向后遍历,获取每一个节点
		while (current) {
			resStr += current.data + '--'
			current = current.next
		}
		return resStr
	}

	// 3.insert方法
	DoubleLinkedList.prototype.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.pre = newNode
				newNode.next = this.head
				this.head = newNode
			} else if (position == this.length) {  // 情况3:position == this.length 
				this.tail.next = newNode
				newNode.pre = this.tail
				this.tail = newNode
			} else { // 情况4:0 < position < this.length
				let current = this.head
				let index = 0

				while (index++ < position) {
					current = current.next
				}

				//修改pos位置前后节点变量的指向
				newNode.next = current
				newNode.pre = current.pre
				current.pre.next = newNode
				current.pre = newNode
			}
		}
		// 4.length+1
		this.length += 1
		return true // 返回true表示插入成功
	}

	// 4.get方法
	DoubleLinkedList.prototype.get = position => {
		// 1.越界判断
		if (position < 0 || position >= this.length) {//获取元素时position不能等于length
			return null
		}

		// 2.获取元素
		let current = null
		let index = 0

		// this.length / 2 > position:从头开始遍历
		if ((this.length / 2) > position) {
			current = this.head
			while (index++ < position) {
				current = current.next
			}
			return current.data
		} else {  // this.length / 2 =< position:从尾开始遍历
			current = this.tail
			index = this.length - 1
			while (index-- > position) {
				current = current.pre
			}
		}

		return current.data
	}

	// 5.indexOf方法
	DoubleLinkedList.prototype.indexOf = data => {
		// 1.定义变量
		let current = this.head
		let index = 0

		// 2.遍历链表,查找与data相同的节点
		while (current) {
			if (current.data == data) {
				return index
			}
			current = current.next
			index++
		}
		return -1
	}

	// 6.update方法
	DoubleLinkedList.prototype.update = (position, newData) => {
		// 1.越界判断
		if (position < 0 || position >= this.length) return false

		// 2.寻找正确的节点
		let index = 0
		let current = this.head
		if (this.length / 2 > position) {
			while (index++ < position) {
				current = current.next
			}
		} else {
			current = this.tail
			index = this.length - 1
			while (index-- > position) {
				current = current.pre
			}
		}

		// 3.修改找到节点的data
		current.data = newData
		return true//表示成功修改
	}

	// 7.removeAt方法
	DoubleLinkedList.prototype.removeAt = position => {
		// 1.越界判断
		if (position < 0 || position >= this.length) return null

		// 2.删除节点
		// 当链表中length == 1
		// 情况1:链表只有一个节点
		let current = this.head//定义在最上面方便以下各种情况返回current.data
		if (this.length == 1) {
			this.head = null
			this.tail = null
		} else {
			// 情况2:删除第一个节点
			if (position == 0) {
				this.head.next.pre = null
				this.head = this.head.next
			} else if (position == this.length - 1) { // 情况3:删除最后一个节点
				current = this.tail // 该情况下返回被删除的最后一个节点
				this.tail.pre.next = null
				this.tail = this.tail.pre
			} else {
				let index = 0
				while (index++ < position) {
					current = current.next
				}
				current.pre.next = current.next
				current.next.pre = current.pre
			}
		}
		// 3.length -= 1
		this.length -= 1
		return current.data // 返回被删除节点的数据
	}

	/*--------------------其他方法-------------------*/
	// 8.remove方法
	DoubleLinkedList.prototype.remove = data => {
		// 1.根据data获取下标值
		let index = this.indexOf(data)

		// 2.根据index删除对应位置的节点
		return this.removeAt(index)
	}

	// 9.isEmpty方法
	DoubleLinkedList.prototype.isEmpty = () => {
		return this.length == 0
	}
	// 10.size方法
	DoubleLinkedList.prototype.size = () => {
		return this.length
	}
	// 11.getHead方法:获取链表的第一个元素
	DoubleLinkedList.prototype.getHead = () => {
		return this.head.data
	}
	// 12.getTail方法:获取链表的最后一个元素
	DoubleLinkedList.prototype.getTail = () => {
		return this.tail.data
	}
}

哈希表(HashTable)

概念

哈希表也叫做散列表。是根据关键字和值(Key-Value)直接进行访问的数据结构。也就是说,它通过关键字 key 和一个映射函数 Hash(key) 计算出对应的值 value,然后把键值对映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做哈希函数(散列函数),用于存放记录的数组叫做 哈希表(散列表)。

特性

优点

  • 哈希表相对于其他表数据结构的主要优势是速度。当键值对数量较大时,这种优势更加明显。当可以预测存入的数量时,哈希表特别有效,因为可以以最佳大小分配一次桶数组,并且永远不用调整大小。
  • 如果键值对是固定的并且提前已知,则可以通过仔细选择散列函数、表大小和内部数据结构来降低平均查找成本。 人们甚至能够设计出一种无冲突甚至完美的散列函数。 在这种情况下,密钥不需要存储在表中。

缺点

  • 虽然哈希表的操作平均时间较少,但一个好的哈希函数的成本可能明显高于顺序列表或搜索树的查找算法的内部循环。 因此当条目数量非常小时,哈希表优势无法体现。
  • 哈希表中的元素遍历只能以某种伪随机顺序进行。
  • 哈希表的局部性较差,因为数据几乎是随机分布的,所以容易导致缓存失效。

实现哈希表

function HashTable() {
	// 定义属性
	this.storage = [];
	this.count = 0;
	this.limit = 5;

	HashTable.prototype.hashFunc = function (str, size) {
		//定义hashCode变量
		var hashCode = 0;
		//根据霍纳算法,计算hashCode的值
		//先将字符串转化为数字编码
		for (var i = 0; i < str.length; i++) {
			hashCode = 37 * hashCode + str.charCodeAt(i)
		}
		//取余操作
		var index = hashCode % size;
		return index;
	}
	//插入和修改操作
	HashTable.prototype.put = function (key, value) {
		//根据key获取对应的index
		var index = this.hashFunc(key, this.limit);
		//根据index取出对应的bucket
		var bucket = this.storage[index];
		//如果值为空,给bucket赋值一个数组
		if (bucket == null) {
			bucket = [];
			this.storage[index] = bucket;
		}
		//判断是否是修改数据
		for (let i = 0; i < bucket.length; i++) {
			var tuple = bucket[i];
			if (tuple[0] === key) {
				tuple[1] = value;
				return;
			}
		}
		//进行添加操作
		bucket.push([key, value]);
		this.count += 1;
		//进行扩容判断
		if (this.count > this.limit * 0.75) {
			this.resize(this.limit * 2);
		}
	}

	//获取操作
	HashTable.prototype.get = function (key) {
		//根据key获取对应的index
		var index = this.hashFunc(key, this.limit);
		//根据index获取对应的bucket
		var bucket = this.storage[index];
		//判断是否为空
		if (bucket == null) {
			return null;
		}
		//线性查找
		for (let i = 0; i < bucket.length; i++) {
			var tuple = bucket[i];
			if (tuple[0] === key) {
				return tuple[1];
			}
		}
		return null;
	}
	//删除操作
	HashTable.prototype.remove = function (key) {
		//根据key获取对应的index
		var index = this.hashFunc(key, this.limit);
		//根据index获取对应的bucket
		var bucket = this.storage[index];
		//判断是否为空
		if (bucket == null) {
			return null;
		}
		//线性查找并通过splice()删除
		for (let i = 0; i < bucket.length; i++) {
			var tuple = bucket[i];
			if (tuple[0] === key) {
				bucket.splice(i, 1);
				this.count -= 1;
				return tuple[1];

				//缩小容量
				if (this.limit > 5 && this.count < this.limit / 2) {
					this.resize(Math.floor(this.limit / 2))
				}
			}
		}
		return null;
	}
	//扩容
	HashTable.prototype.resize = function (newLimit) {
		//保存旧数组的内容
		var oldStorge = this.storage;
		//重置所有属性
		this.storage = [];
		this.count = 0;
		this.limit = newLimit;
		//遍历旧数组的内容
		for (var i = 0; i < oldStorge.length; i++) {
			//取出对应的bucket
			var bucket = oldStorge[i];
			//判断backet是否为空
			if (bucket == null) {
				continue;
			}
			//取出数据重新插入
			for (var j = 0; j < bucket.length; j++) {
				var tuple = bucket[j];
				this.put(tuple[0], tuple[1]);
			}
		}
	}
	HashTable.prototype.isEmpty = function () {
		return this.count === 0;
	}
	HashTable.prototype.size = function () {
		return this.count;
	}
}

概念

栈的英文为(stack)

栈是一个先入后出(FILO-First In Last Out)的有序列表。

栈(stack)是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的一端,为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom)。

根据栈的定义可知,最先放入栈中元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元素最先删除,最先放入的元素最后删除。

特性

优点

提供后进先出的存储方式,添加速度快,允许重复

缺点

只能在一头即栈顶操作数据,存取其他项很慢

实现栈

class Stack {
	constructor() {
		this.items = [];
	}

	// 添加元素到栈顶
	push(element) {
		this.items.push(element);
	}

	// 删除栈底的元素
	pop() {
		return this.items.pop();
	}

	// 返回栈顶的元素
	peek() {
		return this.items[this.items.length - 1];
	}

	// 判断栈里还有没有元素
	isEmpty() {
		return this.items.length === 0;
	}

	// 移除栈里所有的元素
	clear() {
		this.items = [];
	}

	// 返回栈里的元素个数
	size() {
		return this.items.length;
	}
}

队列

概念

和栈相反,队列是一种先进先出(FIFO) 的线性表。

它只允许在表的一端进行插入,而在另一端删除元素。在队列中,允许插入的一端叫做队尾,允许删除的一端叫做队头

特性

优点

提供先进先出的存储方式,添加速度快,允许重复

缺点

只能在一头添加,另一头获取,存取其他项很慢

实现队列

// 队列
function createQueue() {
	// 队列
	let queue = []
	// 入队
	const enQueue = (data) => {
		if (data == null) return
		queue.push(data)
	}
	// 出队
	const deQueue = () => {
		if (queue.length === 0) return
		const data = queue.shift()
		return data
	}
	// 获取列表
	const getQueue = () => {
		// 返回一个克隆的数组,避免外界直接操作
		return Array.from(queue)
	}
	return {
		enQueue,
		deQueue,
		getQueue
	}
}

哈希映射(HashMap)

概念

HashMap,中文名哈希映射,HashMap是一个用于存储Key-Value键值对的集合,每一个键值对也叫做Entry。这些个键值对(Entry)分散存储在一个数组当中,这个数组就是HashMap的主干。HashMap数组每一个元素的初始值都是Null。

HashMap是基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

特性

优点

  • 增删快,增删时结合了链表的增删,避免了数组的移动元素。
  • 查询较快,查询结合了数组的下标访问,进行部分遍历

缺点

  • 采用了数组结构,不可避免需要扩容,扩容是一件消耗很大的事情,重新开辟空间,对每一个元素重新计算 hash 存储;
  • 为了提高效率。内部没有实现锁机制

集合(Set)

概念

Set是一个无重复元素的集合,但不会像数组那样用索引值去访问数组值,通常的做法是检测某个值是否存在这个集合中。

特性

优点:

  • 不重复:可以去除重复

缺点

  • 无序:存取顺序不一致
  • 无索引:没有带索引的方法,所以不能使用普通for循环遍历,也不能通过索引来获取元素

二叉树

概念

二叉树是n个有限元素的集合,该集合或者为空、或者由一个称为根(root)的元素及两个不相交的、被分别称为左子树和右子树的二叉树组成,是有序树。当集合为空时,称该二叉树为空二叉树。在二叉树中,一个元素也称作一个节点

关键术语

  1. 结点之间的关系
    • 若一个结点有子树,那么该结点称为子树根的双亲,子树的根称为该结点的孩子
    • 有相同双亲的结点互为兄弟
    • 一个结点的所有子树上的任何结点都是该结点的后裔
    • 从根结点到某个结点的路径上的所有结点都是该结点的祖先
  2. 结点层:根结点的层定义为第一层,根的孩子为第二层,依此类推
  3. 树的深度:树中最大的结点层
  4. 结点的度:结点拥有的子树的个数
  5. 树的度: 树中最大的结点度
  6. 叶子结点:也叫终端结点,是度为 0 的结点
  7. 分枝结点:度不为0的结点

特性

性质1:在二叉树的第i层上最多有2^(i-1)个结点(i≥1)。

第一层是根结点,只有一个,所以2(1-1)=20=1。 第二层有两个,2(2-1)=21=2。 第三层有四个,2(3-1)=22=4。 第四层有八个,2(4-1)=2^3=8。

性质2:深度为k的二叉树至多有2^k-1个结点(k≥1)。

注意这里一定要看清楚,是2k后再减去1,而不是2(k-1)。以前很多同学不能完全理解,这样去记忆,就容易把性质2与性质1给弄混淆了。 深度为k意思就是有k层的二叉树,我们先来看看简单的。 如果有一层,至多1=21-1个结点。 如果有二层,至多1+2=3=22-1个结点。 如果有三层,至多1+2+4=7=23-1个结点。 如果有四层,至多1+2+4+8=15=2^4-1个结点。

性质3:对任何一棵二叉树,如果其终端结点数为n0,度为2的结点数为n2,则n0=n2+1

终端结点数其实就是叶子结点数,而一棵二叉树,除了叶子结点外,剩下的就是度为1或2的结点数了,我们设n1为度是1的结点数。则树T结点总数n=n0+n1+n2 终端结点数其实就是叶子结点数,而一棵二叉树,除了叶子结点外,剩下的就是度为1或2的结点数了,我们设n1为度是1的结点数。则树T结点总数n=n0+n1+n2 。

性质4:具有n个结点的完全二叉树的深度为|log(2^n)+1|

由满二叉树的定义我们可以知道,深度为k的满二叉树的结点数n一定是2k-1。因为这是最多的结点个数。那么对于n=2k-1倒推得到满二叉树的深度为k=log2(n+1),比如结点数为15的满二叉树,深度为4。

性质5:如果对一棵有n个结点的完全二叉树(其深度为|log(2^n)+1|)的结点按层序编号(从第一层到第层,每层从左到右),对任一结点i(1<=i<=n),有

  1. 如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点。
  2. 如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点2i。
  3. 如果2i+1>n,则结点i无右孩子;否则其右孩子是结点2i+1。

实现二叉树

//二叉树存储结构
class Node{
    constructor(data, left, right){
        this.data = data;
        this.left = left;
        this.right = right;
        this.count = 1;
    }
}