一. 概论
1. 数据结构
队列:
一种遵循先进先出
(FIFO / First In First Out) 原则的一组有序的项;队列在尾部添加新元素,并从头部移除元素。最新添加的元素必须排在队列的末尾。(例如:去食堂排队打饭,排在前面的人先打到饭,先离开;排在后面的人后打到饭,后离开。)
栈:
一种遵从先进后出
(LIFO) 原则的有序集合;新添加的或待删除的元素都保存在栈的末尾,称作栈顶,另一端为栈底。在栈里,新元素都靠近栈顶,旧元素都接近栈底。(例如:往口袋里面装东西,先装进去的放在最下面,后装进去的放在最上面,取出的时候只能从上往下取。)
链表:
存储有序的元素集合,但不同于数组,链表中的元素在内存中并不是连续放置的;每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(指针/链接)组成。
集合:
由一组无序且唯一(即不能重复)的项组成;这个数据结构使用了与有限集合相同的数学概念,但应用在计算机科学的数据结构中。
字典:
以 [键,值] 对为数据形态的数据结构,其中键名用来查询特定元素,类似于 Javascript 中的Object。
哈希表:
根据关键码值(Key value)而直接进行访问的数据结构。通过把关键码值映射到表中某个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫散列表。
给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。
树:
由 n(n>=1)个有限节点组成一个具有层次关系的集合;把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的,基本呈一对多关系,树也可以看做是图的特殊形式。
图:
图是网络结构的抽象模型;图是一组由边连接的节点(顶点);任何二元关系都可以用图来表示,常见的比如:道路图、关系图,呈多对多关系。
2. 算法
排序算法:
冒泡排序(升序)
:逐一比较相邻两个元素,如果前面的元素比后面的元素大则交换两者顺序;元素项向上移动至正确的顺序,好似气泡上升至表面一般,因此得名。(冒泡排序每一轮至少有一个元素会出现在正确位置)
快速排序(升序)
:选择一个基准值,每一个元素与基准值比较。比基准值小的元素放在基准值左边,比基准值大的元素放在基准值右边,左右两边递归执行此操作。通常选择中间的元素作为基准值。
选择排序:
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,以此循环,直至排序完毕。
插入排序:
将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据,此算法适用于少量数据的排序。
归并排序:
将原始序列切分成较小的序列,只到每个小序列无法再切分,然后执行合并,即将小序列归并成大的序列,合并过程进行比较排序,只到最后只有一个排序完毕的大序列,时间复杂度为 O(n log n)。
各种时间复杂度的直观比较:
搜索算法:
顺序搜索
:让目标元素与列表中的每一个元素逐个比较,直到找出与给定元素相同的元素为止,缺点是效率低下。
二分搜索
:在一个有序列表,以中间值为基准拆分为两个子列表,拿目标元素与中间值作比较从而再在目标的子列表中递归此方法,直至找到目标元素。
其他算法:
贪心算法
:在对问题求解时,不考虑全局,总是做出局部最优解的方法。
动态规划
:在对问题求解时,由以求出的局部最优解来推导全局最优解。
复杂度概念
:一个方法在执行的整个生命周期,所需要占用的资源,主要包括:时间资源、空间资源。
二. 数据结构
队列
概念
:一种遵循先进先出
(FIFO / First In First Out) 原则的一组有序的项;队列在尾部添加新元素,并从头部移除元素。最新添加的元素必须排在队列的末尾。
在 Javascript 中实现一个队列类。
1.创建一个类:
class Queue{
constructor(items) {
this.items = items || []
}
// 1. 在末尾添加元素
enqueue(element){
this.items.push(element)
}
// 2. 在头部删除元素
dequeue(){
return this.items.shift()
}
// 3. 获取头部元素
front(){
return this.items[0]
}
// 4. 获取队列长度
get size(){
return this.items.length
}
// 5. 判断是否是空队列
get isEmpty(){
return !this.items.length
}
// 6. 清空队列
clear(){
this.items = []
}
}
2.使用类:
const queue = new Queue() // 类的实例化
queue.isEmpty // true
queue.enqueue('John') // {items: ['John']}
queue.enqueue('Jack') // {items: ['John','Jack']}
queue.enqueue('Camila') // {items: ['John','Jack','Camila']}
queue.size // 3
queue.isEmpty // false
queue.dequeue() // John
queue.dequeue() // Jack
queue.dequeue() // Camila
优先队列
概念
:元素的添加和移除是基于优先级的,不在满足完全意义的先进先出。
例如机场登机的顺序,头等舱和商务舱乘客的优先级要高于经济舱乘客,这些乘客不需要通过正常排队登机。或是去医院看病,医生也会根据病情程度优先处理病情严重的。
在 Javascript 中实现一个队列类:
1.创建一个类:
class PriorityQueue {
constructor() {
this.items = []
}
enqueue(element, priority){
const queueElement = { element, priority }
if (this.isEmpty) {
this.items.push(queueElement)
} else {
// 在列表中找到第一个比后进入的元素的priority大的元素的位置,如果有,将这个元素插入这里。如果没有,将这个元素放在最后面。
const preIndex = this.items.findIndex((item) => queueElement.priority < item.priority)
if (preIndex > -1) {
this.items.splice(preIndex, 0, queueElement)
} else {
this.items.push(queueElement)
}
}
}
dequeue(){
return this.items.shift()
}
front(){
return this.items[0]
}
clear(){
this.items = []
}
get size(){
return this.items.length
}
get isEmpty(){
return !this.items.length
}
print() {
console.log(this.items)
}
}
2.使用类:
const priorityQueue = new PriorityQueue()
priorityQueue.enqueue('Wangjiajia', 2) // {items: [{element: 'Wangjiajia', priority: 2}]}
priorityQueue.enqueue('Wangtongtong', 1) // {items: [{element: 'Wangtongtong', priority: 1},{element: 'Wangjiajia', priority: 2}]}
priorityQueue.enqueue('Davide', 4) // {items: [{element: 'Wangtongtong', priority: 1},{element: 'Wangjiajia', priority: 2},{element: 'Davide', priority: 4}]}
priorityQueue.enqueue('Tom', 3) // {items: [{element: 'Wangtongtong', priority: 1},{element: 'Wangjiajia', priority: 2},{element: 'Tom', priority: 3},{element: 'Davide', priority: 4}]}
priorityQueue.enqueue('James', 2) // {items: [{element: 'Wangtongtong', priority: 1},{element: 'Wangjiajia', priority: 2},{element: 'James', priority: 2},{element: 'Tom', priority: 3},{element: 'Davide', priority: 4}]}
循环队列
概念:
为充分利用向量空间,克服"假溢出"现象将向量空间想象为一个首尾相接的圆环,并称这种向量为循环向量。存储在其中的队列称为循环队列(Circular Queue)。通俗来说:循环队列是把顺序队列首尾相连,把存储队列元素的表从逻辑上看成一个环,成为循环队列。
假溢出:
队列的空间未利用完,但是却造成了元素的溢出。
基于首次实现的队列类,简单实现一个循环引用的示例:
class LoopQueue extends Queue {
constructor(items) {
super(items)
}
getIndex(index) {
const length = this.items.length
return index > length ? (index % length) : index
}
find(index) {
return !this.isEmpty ? this.items[this.getIndex(index)] : null
}
}
使用:
const loopQueue = new LoopQueue(['Surmon'])
loopQueue.enqueue('SkyRover')
loopQueue.enqueue('Even')
loopQueue.enqueue('Alice')
console.log(loopQueue.size, loopQueue.isEmpty) // 4 false
(loopQueue.find(26) // 'Evan'
(loopQueue.find(87651) // 'Alice'
栈:
概念
:一种遵从先进后出
(LIFO) 原则的有序集合;新添加的或待删除的元素都保存在栈的末尾,称作栈顶,另一端为栈底,出去的时候从栈顶开始出去。在栈里,新元素都靠近栈顶,旧元素都接近栈底。`
用javascript基于数组的方法实现一个栈的功能:
class Stack {
constructor() {
this.items = []
}
// 入栈
push(element) {
this.items.push(element)
}
// 出栈
pop() {
return this.items.pop()
}
// 末位
get peek() {
return this.items[this.items.length - 1]
}
// 是否为空栈
get isEmpty() {
return !this.items.length
}
// 尺寸
get size() {
return this.items.length
}
// 清空栈
clear() {
this.items = []
}
// 打印栈数据
print() {
console.log(this.items.toString())
}
}
使用栈:
// 实例化一个栈
const stack = new Stack()
console.log(stack.isEmpty) // true
// 添加元素
stack.push(5) // [5]
stack.push(8) // [5,8]
// 读取属性再添加
console.log(stack.peek) // 8
stack.push(11) // [5,8,11]
stack.pop() // 11
console.log(stack.size) // 3
console.log(stack.isEmpty) // false
链表:
概念:
存储有序的元素集合,但不同于数组,链表中的元素在内存中并不是连续放置的;
每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(指针/链接)组成
。
分类:
单向链表,双向链表,循环链表
链表和数组的区别
:
* 数组在添加或者删除元素的时候需要移动其他元素,链表不需要。链表需要使用指针,因此使用的时候需要额外注意一下。
* 数组可以访问其中任何一个元素,链表需要从头开始迭代,直到找到所需的元素。
链表的常见方法:
1、append(element):
向列表尾部添加一个新的项
2、insert(position, element):
向列表的特定位置插入一个新的项。
3、remove(element):
从列表中移除一项。
4、removeAt(position):
从列表的特定位置移除一项。
5、indexOf(element):
返回元素在列表中的索引。如果列表中没有该元素则返回-1。
6、getElementAt(index):
返回链表中特定位置的元素, 如果不存在这样的元素则返回undefined
7、isEmpty():
如果链表中不包含任何元素,返回true,如果链表长度大于0则返回false。
8、size():
返回链表包含的元素个数。与数组的length属性类似。
9、toString():
由于列表项使用了Node类,就需要重写继承自JavaScript对象默认的toString方法,让其只输出元素的值element。
整体操作方法和数组非常类似,
因为链表本身就是一种可以代替数组的结构.
使用javascript描述一个单向链表
:
单向链表:
生活中的例子:火车就可以看做链表,每一节都是由一节车厢和车厢之间的连接带组成,这个连接带就可以看成是指针。
用javascript来描述一个单向链表:
// 链表节点
class Node {
constructor(element) {
this.element = element
this.next = null
}
}
// 单向链表
class LinkedList {
constructor() {
this.head = null
this.length = 0
}
// 1. 追加元素
// 向链表尾部追加数据可能有两种情况:
// 链表本身为空, 新添加的数据是唯一的节点.
// 链表不为空, 需要向其他节点后面追加节点.
append(element) {
const node = new Node(element)
let current = null
// 链表本身为空, 新添加的数据是唯一的节点.
if (this.head === null) {
this.head = node
} else {
// 链表不为空, 需要向其他节点后面追加节点.
current = this.head
while(current.next) {
current = current.next
}
current.next = node
}
this.length++
}
// 2. 任意位置插入元素
insert(position, element) {
// 1.检测越界问题: 越界插入失败, 返回false
if (position < 0 || position > this.length) return false
// 2.找到正确的位置, 并且插入数据
// 定义要插入的变量newNode, current当前节点, previous上一个节点
let newNode = new Node(element)
let current = this.head // 初始值为head, 对第一个元素的引用
let previous = null // 存储当前current的上一个节点
let index = 0 // 位置
// 3.判断是否列表是否在第一个位置插入
if (position == 0) {
newNode.next = current
this.head = newNode
} else {
while (index++ < position) { // 向前赶, 直到找到当前位置position
previous = current
current = current.next
}
// index === position, 找到要插入的位置
newNode.next = current
previous.next = newNode
}
// 4.length+1
this.length++
}
// 3. 从链表中任意移除一项
removeAny(position) {
//边界检查,越界返回false
if(position < 0 || position > this.length-1){
return false
}
let current = this.head
let previous = null
let index = 0
// 如果删除第一个元素
if(position == 0){
this.head = current.next
}else{
// 不是删除第一个找到删除的位置
while(index++ < position){
previous = current
current = current.next
}
previous.next = current.next
}
this.length--
return current.element
}
// 4. 寻找元素下标
findIndex(element) {
let current = this.head
let index = 0
//遍历链表直到找到data匹配的position
while (current) {
if (element === current.element) {
return index
}
current = current.next
index++
}
return false
}
// 5. 从链表的特定位置移除一项。
remove(element) {
const index = this.indexOf(element)
return this.removeAt(index)
}
// 6. 判断是否为空链表
isEmpty() {
return !this.length
}
// 7. 获取链表长度
size() {
return this.length
}
// 8. 转为字符串
toString() {
let current = this.head
let str = ''
while (current) {
str += ` ${current.element}`
current = current.next
}
return str
}
}
链表类的使用:
const linkedList = new LinkedList()
console.log(linkedList) // LinkedList {head: null, length: 0}
// 1. 在末尾追加元素
linkedList.append(2) // LinkedList {head: {element:2,next:null}, length: 1}
linkedList.append(4) // LinkedList {head: {element:2,next:{element:4,next:null}}, length: 2}
linkedList.append(6) // LinkedList {head: {element:2,next:{element:4,next:{element:6,next:null}}}, length: 3}
// 2. 在任意位置插入一个元素
linkedList.insert(2, 18) // LinkedList {head: {element:2,next:{element:4,next:{element:18,next:{element:6,next:null}}}}, length: 4}
// 3. 从链表中任意位置删除一项
linkedList.removeAny(2) // 18
// 4. 寻找元素下标
linkedList.findIndex(6) // 2
linkedList.findIndex(18) // false
linkedList.findIndex(20) // false
// 5. 从链表的特定位置移除一项。
linkedList.remove(4)
// 6. 判断是否为空链表
linkedList.isEmpty() // false
// 7. 获取链表长度
linkedList.size() // 2
// 8. 转为字符串
linkedList.toString() // ' 2 4 18 6'
双向链表:
概念:
双向链表和普通链表的区别在于,在链表中, 一个节点只有链向下一个节点的链接,而在双向链表中,链接是双向的:一个链向下一个元素, 另一个链向前一个元素,如下图所示:
使用javacsript来实现一个双向链表类:
class Node{
constructor(element){
this.element = element
this.next = null
this.prev = null
}
}
// 双向链表
class DoublyLinkedList {
constructor() {
this.head = null
this.tail = null
this.length = 0
}
// 1. 任意位置插入
insert(position,element){
// 1.1 检测越界问题: 越界插入失败, 返回false
if (position < 0 || position > this.length) return false
let newNode = new Node(element)
let current = this.head // 初始值为head, 对第一个元素的引用
let previous = null // 存储当前current的上一个节点
let index = 0 // 位置
// 1.2 如果插在了首位
if(position == 0){
// 空链表
if(!head){
this.head = node
this.tail = node
}else{
this.head = node
node.next = current
current.prev = node
}
}
// 1.3 如果插在了末位
else if(position == this.length) {
this.tail = node
current.next = node
node.prev = current
}else {
// 如果插在了中间位置
// 找到插入的位置
while(idex++ < position){
previous = current
current = current.next
}
// 插入进去
node.next = current
previous.next = node
node.pre = previous
current.pre = node
this.length++
return true
}
}
// 2. 移除任意位置元素
removeAny(position){
// 2.1 边界检查,越界返回false
if(position < 0 || position > this.length-1) return false
let current = this.head
let previous = null
let index = 0
// 2.2 如果删除的是首位
if(position == 0){
this.head = current.next
this.head.prev = null
}else if(position == this.length - 1){
// 2.3 如果删除的是末位
this.tail = current.pre
this.tail.next = null
}else {
// 2.4 中间项
// 找到删除的位置
while(index++ < position){
previous = current
current = current.next
}
previous.next = current.next
current.prev = current.next.prev
this.length--
return current.element
}
}
}
循环链表:
概念:
循环链表可以像单向链表一样只有单向引用,也可以向双向链表一样具有双向引用。
单向循环链表
:最后一个元素指向下一个元素的指针(tail.next)不是引用null, 而是指向第一个元素(head),如下图所示:
双向循环链表:最后一个元素指向下一个元素的指针tail.next不是nul,而是指向第一个元素(head),同时第一个元素指向前一个元素的指针head.prev不是null,而是最后一个元素tail。如图所示:
链表的优势:无需移动链表中的元素,就能轻松地添加和移除元素。因此,当你需要添加和移除很多元素 时,最好的选择就是链表,而非数组。
集合:
概念
:集合是由一组无序且唯一(不能重复)的项组成的。目前在ES6中已经内置了Set的实现。
集合中常用的一些方法
:
add() 添加元素
delete() 删除元素,返回布尔值,删除成功返回true,删除失败返回false
has() 判断是否含有某个元素,返回布尔值
clear() 清空set数据结构
size 没有括号,返回此结构长度
在ES6中使用集合:
const arr = [1,2,2,3,3,3,4,4,5]
const str = 'wangjiajiawwwjiajiawwwww'
// 1.添加元素
new Set(arr) // Set{1,2,3,4,5}
new Set(str ) // Set{'w', 'a', 'n', 'g', 'j', 'i'}
new Set(arr).add(8) // Set{1,2,3,4,5,8}
// 2.删除元素
new Set(arr).delete(2) // true
new Set(arr).delete(9) // false
// 3.判断是否含有某个元素
new Set(arr).has(2) // true
new Set(arr).has(9) // false
// 4.清空set数据结构
new Set(arr).clear() // undefined
// 5.没有括号,返回此结构长度
new Set(arr).size // 5
在javascript中使用集合:
// hasOwnProperty(propertyName)方法 是用来检测属性是否为对象的自有属性,如果是,返回true,否则返回false; 参数propertyName指要检测的属性名;
// hasOwnProperty() 方法是 Object 的原型方法(也称实例方法),它定义在 Object.prototype 对象之上,所有 Object 的实例对象都会继承 hasOwnProperty() 方法。
class Set {
constructor() {
this.items = {}
}
has(value) {
return this.items.hasOwnProperty(value)
}
add(value) {
if (!this.has(value)) {
this.items[value] = value
return true
}
return false
}
remove(value) {
if (this.has(value)) {
delete this.items[value]
return true
}
return false
}
get size() {
return Object.keys(this.items).length
}
get values() {
return Object.keys(this.items)
}
}
const set = new Set()
set.add(1)
console.log(set.values) // ["1"]
console.log(set.has(1)) // true
console.log(set.size) // 1
set.add(2)
console.log(set.values) // ["1", "2"]
console.log(set.has(2)) // true
console.log(set.size) // 2
set.remove(1)
console.log(set.values) // ["2"]
console.log(set.has(1)) // false
set.remove(2)
console.log(set.values) // []
对集合可以执行如下操作:
并集
:对于给定的两个集合,返回一个包含两个集合中所有元素的新集合。
交集
:对于给定的两个集合,返回一个包含两个集合中共有元素的新集合。
差集
:对于给定的两个集合,返回一个包含所有存在于第一个集合且不存在于第二个集合的元素的新集合。
并集:
并集的数学概念:集合A和B的并集,表示为A∪B,定义如下:A∪B = { x | x∈A V x∈B },意思是x(元素)存在于A中,或x存在于B中。如图:
基于刚才的 Set 类实现一个并集方法:
union(otherSet) {
const unionSet = new Set()
// 先添加其中一个集合的元素放在unionSet中
this.values.forEach((v, i) => unionSet.add(this.values[i]))
// 在把另一个集合的元素放入unionSet中
otherSet.values.forEach((v, i) => unionSet.add(otherSet.values[i]))
return unionSet
}
交集:
并集的数学概念:集合A和B的交集,表示为A∩B,定义如下:A∩B = { x | x∈A ∧ x∈B },意思是x(元素)存在于A中,且x存在于B中。如图:
基于刚才的 Set 类实现一个交集方法:
intersection(otherSet) {
const intersectionSet = new Set()
// 从集合A开始循环判断,如果这个元素也在集合B中,那就说明这个元素是集合A,B公有的。这时候把这个元素放到一个新的集合中
this.values.forEach((v, i) => {
if (otherSet.has(v)) {
intersectionSet.add(v)
}
})
return intersectionSet
}
差集:
差集的数学概念:集合A和B的差集,表示为A-B,定义如下:A-B = { x | x∈A ∧ x∉B },意思是x(元素)存在于A中,且不x存在于B中。如图:
基于刚才的 Set 类实现一个差集 A-B 的方法:
difference(otherSet) {
// 从集合A开始循环判断,如果这个元素不在集合B中。说明这个元素是A私有的,此时把这个元素放入一个新的集合中。
const differenceSet = new Set()
this.values.forEach((v, i) => {
if (!otherSet.has(v)) {
differenceSet.add(v)
}
})
return differenceSet
}
子集:
子集的数学概念:集合A是B的子集,或者说集合B包含了集合A,如图:
基于刚才的 Set 类实现一个子集方法:
// 在这里this代表集合A,otherSet代表集合B
subset(otherSet){
if(this.size > otherSet.size){
return false
}else{
// 只要A里面有一个元素不在B里面就说明A不是B的子集,后面元素不用在判断了
this.values.every(v => !otherSet.has(v))
}
}
字典:
集合、字典、散列表都可以存储不重复的数据。字典以键值对的形式存储数据,类似于javascript中的Object对象。
在javascript中实现字典:
class Dictionary {
constructor() {
this.items = {}
}
set(key, value) {
this.items[key] = value
}
get(key) {
return this.items[key]
}
remove(key) {
delete this.items[key]
}
get keys() {
return Object.keys(this.items)
}
get values() {
/*
也可以使用ES7中的values方法
return Object.values(this.items)
*/
// 在这里我们通过循环生成一个数组并输出
return Object.keys(this.items).reduce((r, c, i) => {
r.push(this.items[c])
return r
}, [])
}
}
使用字典:
const dictionary = new Dictionary()
dictionary.set('Wangjiajia', 'Wangjiajia@email.com')
dictionary.set('Wangtongtong', 'Wangtongtong@email.com')
dictionary.set('davide', 'davide@email.com')
console.log(dictionary) // {items:{'Wangjiajia', 'Wangjiajia@email.com','Wangtongtong', 'Wangtongtong@email.com','davide', 'davide@email.com'}}
console.log(dictionary.keys) // ['Wangjiajia','Wangtongtong','davide']
console.log(dictionary.values) // [Wangjiajia@email.com','Wangtongtong@email.com','davide@email.com']
console.log(dictionary.items) // {'Wangjiajia': 'Wangjiajia@email.com','Wangtongtong': 'Wangtongtong@email.com','davide':'davide@email.com'}