数据结构(数据与数据之间的关系和结构)
简单地说,数据结构是以某种特定的布局方式存储数据的容器。这种“布局方式”决定了数据结构对于某些操作是高效的,而对于其他操作则是低效的。首先我们需要理解各种数据结构,才能在处理实际问题时选取最合适的数据结构。
常见的数据结构
首先列出一些最常见的数据结构,我们将逐一说明:
- 栈
- 队列
- 链表
- 嘻哈表
- 树
数组
数组是最简单、也是使用最广泛的数据结构。栈、队列等其他数据结构均由数组演变而来。下图是一个包含元素(1,2,3和4)的简单数组,数组长度为4。
每个数据元素都关联一个正数值,我们称之为索引,它表明数组中每个元素所在的位置。大部分语言将初始索引定义为零
数组可分为队列和栈等
队列
队列是遵循先进先出的一种数据结构,在尾部添加新元素,并从顶部移除元素。
普通队列
function Queue() {
this.items = [];
}
Queue.prototype = {
enqueue: function (element) {
this.items.push(element);
},
dequeue: function () {
return this.items.shift();
},
front: function () {
return items[0];
},
isEmpty: function () {
return this.items.length === 0;
},
clear: function () {
this.items = [];
},
size: function () {
return this.items.length;
},
print: function () {
console.log(this.items.toString());
}
};
示例(场景)
- 餐厅的叫号网页
- 点击(取号)按钮生成一个号码
- 点击(叫号)按钮显示(请x号取餐)
代码
- 首先选择队列queue做为数据结构
- queue.pus为入队,queue.shift为出队
- 其他事情好做啦,完整代码
栈
栈是一种后进先出的数据结构,也就是说最新添加的项最早被移出;它是一种运算受限的线性表,只能在表头/栈顶进行插入和删除操作。
栈有栈底和栈顶。 向一个栈插入新元素叫入栈(进栈),就是把新元素放入到栈顶的上面,成为新的栈顶; 从一个栈删除元素叫出栈,就是把栈顶的元素删除掉,相邻的成为新栈顶; 也就是说栈里面的元素的插入和删除操作,只在栈顶进行;
打个比方:一队人马走入了一条死胡同,只有一个入口,无出口,想要出去就只能把尾变成首开始出去
- 栈顶是个开口,可以放入元素即push(),移除元素即pop()。
- 栈底封闭,不能操作元素。
图示
栈的方法
js实现栈的方法
//创建一个函数构造器,用来创建对象
var Stack = function(){
//可以以数组模拟栈,首元素为栈底,尾元素为栈顶
var items = []
//入栈 从栈顶进入一个元素
this.push = function(element){
return items.push(element)
}
//出栈 从栈顶移除一个元素
this.pop = function(){
return items.pop()
}
//peek 查看栈顶元素
this.peek = function(){
return items[items.length - 1]
}
//查看栈有多少个元素
this.size = function(){
return items.length
}
//判断栈是否为空
this.isEmpty = function(){
return items.length == 0
}
//清空栈
this.clear = function(){
items = []
}
//查看数组
this.getItems = function(){
return items
}
}
示例
JS函数调栈 call stack 就是一个栈
假设f1调用了f2,f2调用了f3
那么f3返回的结果到f2 ,f2返回的结果到f1
function f1(){let a=1; return a+f2()}
function f2(){let b=2;return b+f3()}
function f3(){let c=3;return c}
f1()
画出压栈,出栈的便知这是【后继先出】的栈
链表(一个链一个)
概念
链表存储有序的元素集合,但不同于数组,链表中的元素在内存中并不是连续放置的。每个 元素由一个存储元素本身的节点和一个指向下一个元素的引用(也称指针或链接)组成。下图展示了链表的结构:
相对于传统的数组,链表的一个好处在于,添加或移除元素的时候不需要移动其他元素。然而,链表需要使用指针,因此实现链表时需要额外注意。 数组的另一个细节是可以直接访问任何位置的任何元素,而要想访问链表中间的一个元素,需要从起点(表头)开始迭代列表直到找到所需的元
实际使用
let array = [1,2,3]
array.__proto__ === Array.prototype
Array.prototype、__proto__ === Object.prototype
从这个角度看对象就是链表
普通链表:
console.log('=====创建链表=====')
const createNode = (value) => {
return {
data: value,
next: null
}
}
//创建节点
const createList = (value) => {
return createNode(value)
}
//添加节点
const appList = (list, value) => {
const node = createNode(value)
let x = list
while (x.next) {
x = x.next
}
x.next = node
return node
}
//删除节点把要删node
//要删除节点的前一个节点 front node
//要上节点的后一个节点after node
//删除节点的思路:前一个节点下一个节点front node.next指向after node节点
//front node.next === after node
const removeList = (list, node) => {
let x = list
let p = node
while (x !== node && x !== null) {
p = x
x = x.next
}
if (x === null) {
return x
} else if (x === p) {
p = x.next
return p
} else {
p.next = x.next
return list
}
}
const froList = (list, fn) => {
let x = list
while (x !== null) {
fn(x)
x = x.next
}
}
//获取
const getList = (list, value) => {
let node = null
froList(list, (n) => {
if (n.data === value) {
node = n
}
})
return node
}
const list = createList(10)
const node3 = list
const node = appList(list, 50)
const node1 = appList(list, 30)
const node2 = appList(list, 60)
console.log(list)
const node5 = removeList(list, node1)
const node6 = removeList(list, node3)
console.log("rmlist")
console.log(node5)
console.log(node6)
froList(list, (x) => {
console.log(x.data)
})
console.log(getList(list, 50))
双向链表(每一个节点的prototype都指向上一个节点)
class Node {
constructor(element) {
this.element = element;
this.prev = null;
this.next = null;
}
}
// 双向链表
class DoubleLinkedList {
constructor() {
this.head = null;
this.tail = null;
this.length = 0;
}
// 任意位置插入元素
insert(position, element) {
if (position >= 0 && position <= ehis.length) {
let node = new Node(element);
let current = this.head;
let previous = null;
this.index = 0;
// 首位
if (position === 0) {
if (!head) {
this.head = node;
this.tail = node;
} else {
node.next = current;
this.head = node;
current.prev = node;
}
} else if (position === this.length) { // 末尾
current = this.tail;
current.next = node;
node.prev = current;
this.tail = node;
} else { // 中间
while(index++ < position) {
previous = current;
current = current.next;
}
node.next = current;
previous.next = node;
current.prev = node;
node.prev = previous;
}
this.length++;
return true;
}
return false;
}
// 移除指定位置元素
removeAt(position) {
if (position > -1 && position < this.length) {
let current = this.head;
let previous = null;
let index = 0;
// 首位
if (position === 0) {
this.head = this.head.next
this.head.prev = null
if (this.length === 1) {
this.tail = null
}
} else if (position === this.length - 1) { // 末位
this.tail = this.tail.prev
this.tail.next = null
} else { // 中位
while (index++ < position) {
previous = current
current = current.next
}
previous.next = current.next
current.next.prev = previous
}
this.length--;
return current.element;
} else {
return null;
}
}
// 其他方法
}
循环链表 (最后一个节点指向头节点)
如图所示
哈希表概念
1.哈希化:将大数字转化为数组范围内下表的过程,我们称之为哈希化。(对大数字取余)
2.哈希函数:通常我们会将单词转化成大数字,大数字在进行哈希化的代码实现放在一个函数中,这个韩式称为哈希函数。
3.哈希表:最终的数据插入到的这个数组,对整个结构的封装,我们称之为是一个哈希表。
数组的缺点
-
数组进行插入操作时,效率比较低。
-
数组基于索引去查找的操作效率非常高,基于内容去查找效率很低。
-
数组进行删除操作,效率也不高。
.哈希表是基于 数组 实现的,但相对于数组有很多优势
1.它可以提供非常快速的 插入-删除-查找 操作
2.无论多少数据,插入和删除需要接近常量的时间。即O(1)的时间级
3.哈希表的速度比树还要快,基本可以瞬间找到想要的元素。
4.哈希表相对于树来说编码要容易
.哈希表对于数组的一些不足
1.哈希表中的数据是没有顺序的,所以不能以一种固定的方式来遍历其中的元素。
2.通常情况下,哈希表中的key是不允许重复的,不能放置相同的key,用于保存不同的元素。
.哈希表的实质
1.哈希表不同于(数组和链表,甚至于树可以画出他的结构)。
2.他的结构就是数组,但他神奇的地方在于它对下标值的一种变换,这种变换称为 哈希函数 , 通过哈希函数可以获取到 HashCode。
一个 哈希表(hash table 或hash map) 是一种实现 关联数组(associative array) 的抽象数据;类型, 该结构可以将 键映射到值。
树(一个链多个)
树(英语:tree)是一种抽象数据类型或是实现这种抽象数据类型的数据结构,用来模拟具有树状结构性质的数据集合。它是由n(n>0)个有限节点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点
①每个节点有零个或多个子节点; ②没有父节点的节点称为根节点; ③每一个非根节点有且只有一个父节点; ④除了根节点外,每个子节点可以分为多个不相交的子树;
树的实现
const shu = (value) => {
return {
data: value,
children: null,
parent: null
}
}
const childNode = (node, value) => {
const newNode = {
data: value,
children: null,
parent: node
}
node.children = node.children || []
node.children.push(newNode)
return newNode
}
//所有攻击点
const forNode = (node, fn) => {
fn(node)
if (!node.children) return
for (let i = 0; i < node.children.length; i++) {
forNode(node.children[i], fn)
}
}
const findNode = (shu, node) => {
if (shu === node) return shu
if (shu.children) {
for (let i = 0; i < shu.children.length; i++) {
if (shu.children[i] === node) {
return shu.children[i]
}
}
return undefined
} else {
return undefined
}
}
//删除节点
//找到父节点的所有节点siblings
//找到要删除节点的下标
//根据下标删除节点
const reMode = (shu, node) => {
const siblings = node.parent.children
let index = 0
for (let i = 0; i < siblings.length; i++) {
if (siblings[i] === node) {
index = i
}
}
siblings.splice(index, 1)
}
const shu1 = shu(20)
const node = childNode(shu1, 10)
console.log(node)
const node2 = childNode(shu1, 20)
childNode(shu1, 30)
childNode(node, 40)
childNode(node, 60)
childNode(node, 70)
childNode(node, 80)
// console.log(shu1)
// console.log(shu1)
forNode(shu1, (node) => {
console.log(node.data)
})
// console.log(findNode(shu1, node2))
console.log("删除前")
console.log(shu1)
reMode(shu1, node)
console.log("删除后")
console.log(shu1)