定义
存储和组织数据
的方式,精心选择的数据结构可以带来最优效率的算法
高效
的方式组织和存储
呢?
相关操作
方便实现:
结论
效率
,跟数据的组织方式
有关不同的数据结构
的不同操作
性能是不同的直接或者间接
的使用上述常见的数据结构算法
,执行效率
是不一样的数据的存储方式
会影响效率,算法的优劣
也会影响着效率解决问题的办法/步骤逻辑
应用层面
,那么数据结构和算法显得没有那么重要设计层面
,那么数据结构和算法就非常重要为什么使用数组?
// 使用数组来保存名字
var names = ['Tom','zx','zs','ls']
创建和初始化数组
// 创建和初始化数组
var daysOfWeek = new Array()
var daysOfWeek = new Array(7)
var daysOfWeek = new Array('Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday')
代码解析:
1. 使用new关键字,就能简单地声明并初始化一个数组
2. 还可以使用这种方式创建一个指定长度的数组
3. 也可以直接将数组元素作为参数传递给它的构造器
4. 用new创建数组并不是最好的方式,在JavaScript中创建一个数组,只用中括号`[]`的形式就行
// 使用中括号`[]`创建数组
var daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
数组长度和遍历数组
length
属性// 获取数组的长度
alert(daysOfWeek.length)
// 也可以通过下标值来遍历数组
// 普通for方式遍历数组
for (var i = 0; i < daysOfWeek.length; i++) {
alert(daysOfWeek[i])
}
// 通过foreach遍历数组
daysOfWeek.forEach(function (value) {
alert(value)
})
练习:
1. 求斐波那契数列的前20个数字,并且放在数组中
2. 斐波那契数列(数列第一个数字是1,第二个数字也是1,第三项是前两项的和)
// 求斐波那契数列的前20个数字
var fibonacci = []
fibonacci[0] = 1
fibonacci[1] = 1
for (var i = 2; i < 20; i++) {
fibonacci[i] = fibonacci[i - 1] + fibonacci[i - 2]
}
alert(fibonacci)
添加元素
- 假如有一个数组:numbers,初始化0-9
// 初始化一个数组
var numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
===============================================================
- 添加一个元素到数组的最后位置
// 添加一个元素到数组的最后位置
// 方式一:
numbers[numbers.length] = 10
// 方式二:
numbers.push(11)
numbers.push(12, 13)
alert(numbers)
===============================================================
- 在数组首位插入一个元素
// 在数组首位插入一个元素
for (var i = numbers.length; i > 0; i--) {
numbers[i] = numbers[i-1]
}
number[0] = -1
alert(numbers)// -1,0,,1,2,3,4,5,6,7,8,9,10,11,12,13
===============================================================
- 在数组首位插入数据可以直接使用unshift方法
// 通过unshift在首位插入数据
numbers.unshift(-2)
numbers.unshift(-4, -3)
alert(numbers) // -4,-3,-2,-1,0,1,2,3,4,5,6,7,8,9,10,11,12,13
- 性能问题
- 性能并不算非常高
- 这也是数组和链表相对比的一个劣势:在中间位置插入元素的效率比链表低
- 如果希望删除数组最后的元素,可以使用`pop()`方法
// 删除最后的元素
numbers.pop()
alert(numbers) // -4,-3,-2,-1,0,1,2,3,4,5,6,7,8,9,10,11,12
===============================================================
- 如果希望移除的首位元素
// 删除首位的元素
for (var i = 0; i < numbers.length; i++) {
numbers[i] = numbers[i+1]
}
numbers.pop()
alert(numbers)
===============================================================
- 可以直接使用shift方法来实现
numbers.shift()
alert(numbers)
任意位置
- 前面学习的主要是在数组开头和结尾处添加和删除数据
- 那如果希望在数组的中间位置进行一些操作应该怎么办呢?
===============================================================
- 通过splice删除数据
// 删除指定位置的几个元素
numbers.splice(5, 3)
alert(numbers) // -4,-3,-2,-1,0,4,5,6,7,8,9,10,11,12,13
- 代码解析
- 上面的代码会删除索引为5, 6, 7位置的元素
- 第一个参数表示索引起始的位置为5(其实是第6个元素, 因为索引从0开始的), 删除3个元素
===============================================================
- 如果希望使用splice来插入数据呢?
// 插入指定位置元素
numbers.splice(5, 0, 3, 2, 1)
alert(numbers) // -4,-3,-2,-1,0,3,2,1,4,5,6,7,8,9,10,11,12,13
- 代码解析
- 上面的代码会从索引为5的位置开始插入数据,其他数据依次向后位移
- 第一个参数依然是索引值为5(第六个位置)
- 第二个参数为0时表示不是删除数据, 而是插入数据.
- 后面紧跟的是在这个位置要插入的数据, 可以是其他类型, 比如"a", "b", "c"
===============================================================
- 如果希望使用splice来修改数据呢?
// 修改指定位置的元素
numbers.splice(5, 3, "a", "b", "c")
alert(numbers) // -4,-3,-2,-1,0,a,b,c,4,5,6,7,8,9,10,11,12,13
- 代码解析
- 上面的代码会从索引5的位置开始修改数据, 修改多少个呢? 第二个参数来决定的
- 第一个参数依然是索引的位置为5(第六个位置)
- 第二个参数是要将数组中多少个元素给替换掉, 我们这里是3个
- 后面跟着的就是要替换的元素
常见方法
方法名 | 方法描述 |
---|---|
concat |
连接2个或更多数组,并返回结果 |
every |
对数组中的每一项运行给定函数,如果该函数对每一项都返回 true,则返回 true,否则返回 false |
filter |
对数组中的每一项运行给定函数,返回该函数会返回 true 的项组成的数组 |
forEach |
对数组中的每一项运行给定函数,这个方法没有返回值 |
join |
将所有的数组元素连接成一个字符串 |
indexOf |
返回第一个与给定参数相等的数组元素的索引,没有找到则返回-1 |
lastIndexOf |
返回在数组中搜索到的与给定参数相等的元素的索引里最大的值 |
map |
对数组中的每一项运行给定函数,返回每次函数调用的结果组成的数组 |
reverse |
颠倒数组中元素的顺序,原先第一个元素现在变成最后一个,同样原先的最后一个元素变成了现在的第一个 |
slice |
传入索引值,将数组里对应索引范围内的元素作为新数组返回 |
some |
对数组中的每一项运行给定函数,如果任一项返回 true,则结果为true, 并且迭代结束 |
sort |
按照字母顺序对数组排序,支持传入指定排序方法的函数作为参数 |
toString |
将数组作为字符串返回 |
valueOf |
和 toString 类似,将数组作为字符串返回 |
数组合并
- 数组的合并非常简单,使用concat即可(也可以直接+进行合并)
// 数组的合并
var nums1 = [1, 2, 3]
var nums2 = [100, 200, 300]
var newNums = nums1.concat(nums2)
alert(newNums) // 1,2,3,100,200,300
newNums = nums1 + nums2
alert(newNums) // 1,2,3,100,200,300
迭代方法
- every()方法
- every()方法是将数组中每一个元素传入到一个函数中,该函数返回 true/false
- 如果函数中每一个元素都返回 true,那么结果为 true,有一个为 false,那么结果为 false
- every()练习
- 判断一组元素中是否都包含某一个字符
// 定义数组
var names = ['abc', 'cb', 'mba', 'dna']
// 判断数组的元素是否都包含a字符
var flag = names.every(function (t) {
return t.indexOf('a') != -1
})
alert(flag)
========================================================================================
- some()方法
- some()方法是将数组中每一个元素传入到一个函数中,该函数返回 true/false
- 但是和 every 不同的是,一旦有一次函数返回了 true ,那么迭代就会结束,并且结果为 true
- some()练习
// 定义数组
var names = ['abc', 'cb', 'mba', 'dna']
// 判断数组中是否包含有a字符的字符
var flag = names.some(function (t) {
alert(t)
return t.indexOf('a') != -1
})
alert(flag)
========================================================================================
- forEach()方法
- forEach()方法仅仅是一种快速迭代数组的方式
- 该方法不需要返回值
- forEach 的使用
// 定义数组
var names = ['abc', 'cb', 'mba', 'dna']
// forEach的使用
names.forEach(function (t) {
alert(t)
})
========================================================================================
- filter()方法
- filter()方法是一种过滤的函数
- 首先会遍历数组中每一个元素传入到函数中
- 函数的结果返回 true,那么这个元素会被添加到最新的数组中,返回 false,则忽略该元素
- 最终会形成一个新的数组,该数组就是 filter() 方法的返回值
- filter()的练习
// 定义数组
var name = ['abc', 'cb', 'mba', 'dna']
// 获取names中所有包含'a'字符的元素
var newNames = names.filter(function (t) {
return t.indexOf('a') != -1
})
alert(newNames)
========================================================================================
- map()方法
- map()方法提供的是一种映射函数
- 首先会遍历数组中每一个元素传入到函数中
- 元素会经过函数中的指令进行各种变换, 生成新的元素, 并且将新的元素返回
- 最终会将返回的所有元素形成一个新的数组, 该数组就是map()方法的返回值
- map()练习
// 定义数组
var names = ["abc", "cb", "mba", "dna"]
// 在names中所有的元素后面拼接-abc
var newNames = names.map(function (t) {
return t + "-abc"
})
alert(newNames)
reduce方法
arr.reduce(callback[, initialValue])
- 参数
- callback(一个在数组中每一项上调用的函数,接受四个函数:)
- previousValue(上一次调用回调函数时的返回值,或者初始值)
- currentValue(当前正在处理的数组元素)
- currentIndex(当前正在处理的数组元素下标)
- array(调用reduce()方法的数组)
- initialValue(可选的初始值。作为第一次调用回调函数时传给previousValue的值)
========================================================================================
- reduce 练习
- 求一个数字中数字的累加和
- 使用 for 实现:
// 1.定义数组
var numbers = [1, 2, 3, 4]
// 2.for实现累加
var total = 0
for (var i = 0; i < numbers.length; i++) {
total += numbers[i]
}
alert(total) // 10
========================================================================================
- 使用forEach简化for循环
- 相对于for循环, forEach更符合我们的思维(遍历数组中的元素)
// 3.使用forEach
var total = 0
numbers.forEach(function (t) {
total += t
})
alert(total)
========================================================================================
- 使用reduce方法实现
// 4.使用reduce方法
var total = numbers.reduce(function (pre, cur) {
return pre + cur
})
alert(total)
- 代码解析:
- pre中每次传入的参数是不固定的, 而是上次执行函数时的结果保存在了pre中
- 第一次执行时, pre为0, cur为1
- 第二次执行时, pre为1 (0+1, 上次函数执行的结果), cur为2
- 第三次执行时, pre为3 (1+2, 上次函数执行的结果), cur为3
- 第四次执行时, pre为6 (3+3, 上次函数执行的结果), cur为4
- 当cur为4时, 数组中的元素遍历完了, 就直接将第四次的结果, 作为reduce函数的返回值进行返回.
- reduce优势
- 通过代码会发现, 不需要在调用函数前先定义一个变量, 只需要一个变量来接收方法最终的参数即可.
- 优势在于reduce方法有返回值, 而forEach没有.
- 如果reduce方法有返回值, 那么reduce方法本身就可以作为参数直接传递给另外一个需要reduce返回值的作为参数的函数. 而forEach中只能先将每次函数的结果保存在一个变量, 最后再将变量传入到参数中.
- 这就是函数式编程. 几乎每个可以使用函数式编程的语言都有reduce这个方法
Java
的ArrayList
)
不能
存放不同的数据类型
,因此所有在封装时通常存放在数组中的是Object
类型容量不会自动改变
(需要进行扩容
操作)插入和删除
操作性能比较低
栈也是一种非常常见
的数据结构,并且在程序中的应用非常广泛
数组
线性结构
, 并且可以在数组的任意位置
插入和删除数据.任意性
加以限制
栈和队列
就是比较常见的受限的线性结构
栈(stack
),它是一种运算受限的线性表,后进先出(LIFO)
LIFO(last in first out)
表示就是后进入的元素, 第一个弹出栈空间表的一端
进行插入和删除运算。这一端被称为栈顶
,相对地,把另一端称为栈底
进栈、入栈
或压栈
,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素出栈或退栈
,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素函数调用栈
栈底A->B->C->D栈顶
数组
实现链表
实现栈的创建
- 先创建一个栈的类,用于封装栈相关操作
// 栈类
function Stack() {
// 1. 栈中的属性
var items = []
// 2. 栈的相关操作
}
- 代码解析:
- 创建了一个Stack构造函数, 用户创建栈的类.
- 在构造函数中, 定义了一个变量, 这个变量可以用于保存当前栈对象中所有的元素.
- 这个变量是一个数组类型. 无论是压栈操作还是出栈操作, 都是从数组中添加和删除元素.
- 栈有一些相关的操作方法, 通常无论是什么语言, 操作都是比较类似的.
栈的常见操作
push(element)
: 添加一个新元素到栈顶位置.pop()
:移除栈顶的元素,同时返回被移除的元素。peek()
:返回栈顶的元素,不对栈做任何修改(这个方法不会移除栈顶的元素,仅仅返回它)。isEmpty()
:如果栈里没有任何元素就返回true,否则返回false。clear()
:移除栈里的所有元素。size()
:返回栈里的元素个数。这个方法和数组的length属性很类似。2.1 将元素压入栈(push方法)
- 注意: 我们的实现是将最新的元素放在了数组的末尾, 那么数组末尾的元素就是我们的栈顶元素
方式一 给某一个对象的实例添加了一个方法(不推荐)
this.push = function (element) {
}
方式二 给整个类添加了一个方法(更加节省内存,性能更高)
Stack.prototype.push = function (element) {
this.items.push(element)
}
=================================================================
2.2 从栈中取出元素(pop方法)
- 注意: 出栈操作应该是将栈顶的元素删除, 并且返回.
- 因此, 我们这里直接从数组中删除最后一个元素, 并且将该元素返回就可以了
Stack.prototype.pop = function () {
return this.items.pop()
}
=================================================================
2.3 查看一下栈顶元素(peek方法)
- peek方法是一个比较常见的方法, 主要目的是看一眼栈顶的元素.
- 注意: 和pop不同, peek仅仅的瞥一眼栈顶的元素, 并不需要将这个元素从栈顶弹出.
Stack.prototype.peek = function () {
return this.items[this.items.length -1]
}
=================================================================
2.4 判断栈是否为空(isEmpty方法)
- isEmpty方法用户判断栈中是否有元素.
- 实现起来非常简单, 直接判断数组中的元素个数是为0, 为0返回true, 否则返回false
Stack.prototype.isEmpty = function () {
return this.items.length == 0
}
=================================================================
2.5 获取栈中元素的个数(size方法)
- size方法是获取栈中元素的个数.
- 因为我们使用的是数组来作为栈的底层实现的, 所以直接获取数组的长度即可.(也可以使用链表作为栈的顶层实现)
Stack.prototype.size = function () {
return this.items.length
}
=================================================================
2.6 toString方法
Stack.prototype.toString = function () {
var resultString = ''
for (var i = 0; i < this.items.length; i++) {
resultString += this.items[i] + ' '
}
return resultString
}
}
=================================================================
// 栈的使用
var s = new Stack()
s.push(20)
s.push(10)
s.push(30)
s.push(50)
alert(s) // 20 10 30 50
s.pop()
s.pop()
alert(s) // 20 10
alert(s.peek()) // 10
alert(s.isEmpty()) // false
alert(s.size()) // 2
为什么需要进制转换
如何实现进制转换
举例
:代码实现
function Stack() {
this.items = [];
Stack.prototype.push = function(element) {
this.items.push(element)
}
Stack.prototype.pop = function(element) {
return this.items.pop()
}
Stack.prototype.isEmpty = function() {
return this.items.length == 0
}
}
// 函数:将十进制转换二进制
function dec2bin(decNumber) {
// 1. 定义栈对象
var stack = new Stack()
// 2. 循环操作
// 从最开始的decNumber除2,所得余数压入栈中,所得正数结果用于下一次计算
// 当被除数小于等于0时,循环终止
// 不确定循环次数,用while循环
while (decNumber > 0) {
// 2.1 获取余数,并放入到栈中
stack.push(decNumber % 2)
// 2.2 获取整除后的结果,作为下一次运行的数字
decNumber = Math.floor(decNumber / 2)
}
// 3. 从栈中取出0和1
var binaryString = ''
while (!stack.isEmpty()) {
binaryString += stack.pop()
}
return binaryString
}
// 测试十进制转二进制函数
console.log(dec2bin(100));
队列结构
先进先出
(FIFO First In First Out)
受限的线性结构
前端
(front)进行删除操作,而在表的后端
(rear)进行插入操作队列图解
队列的应用
打印队列
线程队列
队列类的创建
// 封装队列类
function Queue() {
// 属性
this.items = []
// 方法
}
- 代码解析:
- 创建一个Queue构造函数, 用户创建队列的类.
- 在构造函数中, 定义了一个变量, 这个变量可以用于保存当前队列对象中所有的元素.
- 这个变量是一个数组类型. 之后在队列中添加元素或者删除元素, 都是在这个数组中完成的.
- 队列和栈一样, 有一些相关的操作方法, 通常无论是什么语言, 操作都是比较类似的.
队列常见操作
enqueue(element)
:向队列尾部添加一个(或多个)新的项dequeue()
:移除队列的第一(即排在队列最前面的)项,并返回被移除的元素front()
:返回队列中第一个元素——最先被添加,也将是最先被移除的元素。队列不做任何变动(不移除元素,只返回元素信息——与Stack类的peek方法非常类似)isEmpty()
:如果队列中不包含任何元素,返回true,否则返回falsesize()
:返回队列包含的元素个数,与数组的length属性类似// 方法
// 1. 将元素加入到队列中
Queue.prototype.enqueue = function (element) {
this.items.push(element)
}
// 2. 从队列中删除前端元素
Queue.prototype.dequeue = function () {
return this.items.shift()
}
// 3. 查看前端的元素
Queue.prototype.front = function () {
return this.items[0]
}
// 4. 查看队列是否为空
Queue.prototype.isEmpty = function () {
return this.items.length == 0
}
// 5. 查看队列中元素的个数
Queue.prototype.size = function () {
return this.items.length
}
// 6. toString方法
Queue.prototype.toString = function () {
var resultString = ''
for (var i = 0; i < this.items.length; i++) {
resultString += this.items[i] + ''
}
return resultString
}
队列的使用
// 使用队列
var queue = new Queue()
// 在队列中添加元素
queue.enqueue("abc")
queue.enqueue("cba")
queue.enqueue("nba")
// 查看一下队列前端元素
alert(queue.front())
// 查看队列是否为空和元素个数
alert(queue.isEmpty())
alert(queue.size())
// 从队列中删除元素
alert(queue.dequeue())
alert(queue.dequeue())
alert(queue.dequeue())
击鼓传花规则
代码实现
// 面试题:击鼓传花
function passGame(nameList, num) {
// 1. 创建一个队列结构
var queue = new Queue()
// 2. 将所有人依次加入到队列中
for (var i = 0; i < nameList.length; i++) {
queue.enqueue(nameList[i])
}
// 3. 开始数数字
while (queue.size() > 1) {
// 不是num的时候,重新加入到队列的末尾
// 是num这个数字的时候,将其从队列中删除
// 3.1 num数字之前的人重新放入到队列的末尾
for (var i = 0; i < num - 1; i++) {
queue.enqueue(queue.dequeue())
}
// 3.2 num对应这个人,直接从队列中删除掉
queue.dequeue()
}
// 4. 获取剩下的那个人
alert(queue.size())
var endName = queue.front()
alert('最终剩下的人:' + endName)
return nameList.indexOf(endName)
}
// 测试击鼓传花
var names = ['John','Jack','Camila','Ingrid','Carl'];
var index = passGame(names, 7) // 数到8的人淘汰
alert("最终位置:" + index)
特点
该数据的优先级
比较
正确的位置
主要考虑问题
优先级队列的实现
代码实现
// 封装优先级队列
function PriorityQueue() {
// 在PriorityQueue重新创建了一个类:可以理解成内部类
function QueueElement(element, priority) {
this.element = element
this.priority = priority
}
// 封装属性
this.items = []
// 实现插入方法
PriorityQueue.prototype.enqueue = function (element, priority) {
// 1. 创建QueueElement对象
var queueElement = new QueueElement(element, priority)
// 2. 判断队列是否为空
if (this.items.length == 0) {
this.items.push(queueElement)
} else {
var added = false
for (var i = 0; i < this.items.length; i++) {
if(queueElement.priority < this.items[i].priority) {
this.items.splice(i, 0, queueElement)
added = true
break
}
}
if (!added) {
this.items.push(queueElement)
}
}
}
// 2. 从队列中删除前端元素
PriorityQueue.prototype.dequeue = function () {
return this.items.shift()
}
// 3. 查看前端的元素
PriorityQueue.prototype.front = function () {
return this.items[0]
}
// 4. 查看队列是否为空
PriorityQueue.prototype.isEmpty = function () {
return this.items.length == 0
}
// 5. 查看队列中元素的个数
PriorityQueue.prototype.size = function () {
return this.items.length
}
// 6. toString方法
PriorityQueue.prototype.toString = function () {
var resultString = ''
for (var i = 0; i < this.items.length; i++) {
resultString += this.items[i].element + '-' + this.items[i].priority + ' '
}
return resultString
}
}
// 测试代码
var pq = new PriorityQueue()
// 添加元素
pq.enqueue("abc", 10)
pq.enqueue("cba", 5)
pq.enqueue("nba", 12)
pq.enqueue("mba", 3)
// 遍历所有的元素
var size = pq.size()
for (var i = 0; i < size; i++) {
var item = pq.dequeue()
alert(item.element + "-" + item.priority)
}
代码解析
数组
最常用
的数据结构数组结构
, 这种数据结构非常方便,提供了一个便利的[]
语法来访问它的元素数组的缺点
连续的内存空间
(一整块的内存), 并且大小是固定的(大多数编程语言数组都是固定的), 所以当当前数组不能满足容量需求
时, 需要扩容
. (一般情况下是申请一个更大的数组, 比如2倍. 然后将原数组中的元素复制过去)Array
类方法可以做这些事,但背后的原理依然是这样)链表
链表
不必是连续的空间
元素本身的节点
和一个指向下一个元素的引用
(有些语言称为指针或者链接)组成.链表的优点
内存动态管理
确定大小
, 并且大小可以无限的延伸
下去插入和删除
数据时, 时间复杂度
可以达到O(1). 相对数组效率高很多链表的缺点
从头开始访问
.(无法跳过第一个元素访问任何一个元素).创建链表类
// 封装链表类
function LinkedList() {
// 内部的类:节点类
function Node(data) {
this.data = data
this.next = null
}
// 属性
this.head = null
this.length = 0
}
代码解析
常见操作
append(element)
:向列表尾部添加一个新的项insert(position, element)
:向列表的特定位置插入一个新的项get(position)
:获取对应位置的元素indexOf(element)
:返回元素在列表中的索引。如果列表中没有该元素则返回-1update(position, element)
:修改某个位置的元素remove(element)
:从列表中移除一项removeAt(position)
:从列表的特定位置移除一项isEmpty()
:如果链表中不包含任何元素,返回true,如果链表长度大于0则返回falsesize()
:返回链表包含的元素个数。与数组的length属性类似toString()
:由于列表项使用了Node类,就需要重写继承自JavaScript对象默认的toString方法,让其只输出元素的值append()方法
// 1. 追加方法
LinkedList.prototype.append = function (data) {
// 1. 创建新的节点
var newNode = new Node(data)
// 2. 判断是否添加的是第一个节点
if (this.length == 0) {
// 2.1 是第一个节点
this.head = newNode
} else {
// 2.2 不是第一个节点
// 找到最后一个节点
var current = this.head
while (current.next) {
current = current.next
}
// 最后节点的next指向新的节点
current.next = newNode
}
// 3. length+1
this.length += 1
}
toString()方法
代码分析
// 2. toString
LinkedList.prototype.toString = function () {
// 1. 定义变量
var current = this.head
var listString = ''
// 2. 循环获取一个个的节点
while (current) {
listString += current.data + ' '
current = current.next
}
return listString
}
insert()方法
代码分析
// 3. insert方法----任意位置插入
LinkedList.prototype.insert = function (position, data) {
// 1. 对 position 进行越界判断
if(position < 0 || position > this.length) return false
// 2. 根据 data 创建 newNode
var newNode = new Node(data)
// 3. 判断插入的位置是否是第一个
if (position == 0) {
newNode.next = this.head
this.head = newNode
} else {
var index = 0
var current = this.head
var previous = null
while (index++ < position) {
previous = current
current = current.next
}
newNode.next = current
previous.next = newNode
}
// 4. length + 1
this.length += 1
return true
}
get()方法
// 4. get方法
LinkedList.prototype.get = function (position) {
// 1. 越界判断
if (position < 0 || position >= this.length) return null
// 2. 获取对应的data
var current = this.head
var index = 0
while (index++ < position) {
current = current.next
}
return current.data
}
indexOf()方法
代码解析
// 5. indexOf方法
LinkedList.prototype.indexOf = function (data) {
// 1. 定义变量
var current = this.head
var index = 0
// 2. 开始查找
while (current) {
if (current.data == data) {
return index
}
current = current.next
index += 1
}
// 3. 找到最后没有找到,返回 -1
return -1
}
update()方法
// 6. update方法
LinkedList.prototype.update = function (position, newData) {
// 1. 越界判断
if (position < 0 || positon >= this.length) return false
// 2. 查找正确的节点
var current = this.head
var index = 0
while (index++ < positon) {
current = current.next
}
// 3. 将 position 位置的 node 的 data 修改成 newData
current.data = newData
return true
}
removeAt()方法
代码分析
// 根据位置移除节点
LinkedList.prototype.removeAt = function (position) {
// 1.检测越界问题: 越界移除失败, 返回null
if (position < 0 || position >= this.length) return null
// 2.定义变量, 保存信息
var current = this.head
var previous = null
var index = 0
// 3.判断是否是移除第一项
if (position === 0) {
this.head = current.next
} else {
while (index++ < position) {
previous = current
current = current.next
}
previous.next = current.next
}
// 4.length-1
this.length--
// 5.返回移除的数据
return current.data
}
remove()方法
代码分析
// 8. remove方法
LinkedList.prototype.remove = function (data) {
// 1. 获取data在列表中的位置
var position = this.indexOf(data)
// 2. 根据位置信息,删除节点
return this.removeAt(position)
}
isEmpty()方法
// 9. isEmpty方法
LinkedList.prototype.isEmpty = function () {
return this.length == 0
}
size()方法
// 10. size()方法
LinkedList.prototype.size = function () {
return this.length
}
单向链表
从头遍历到尾
或者从尾遍历到头
(一般从头到尾)单向
的引用
缺点
下一个节点
, 但是回到前一个节点
是很难的双向链表
从头遍历到尾
, 又可以从尾遍历到头
双向
的向前连接的引用
, 也有一个向后连接的引用
缺点
处理四个节点
的引用, 而不是两个占用内存空间更大
一些.双向连接图解
双向链表特点
代码分析
// 封装双向链表
function DoublyLinkedList () {
// 内部类:节点类
function Node(data) {
this.data = data
this.prev = null
this.next = null
}
// 属性
this.head = null
this.tail = null
this.length = 0
}
双向链表常见操作
append(element)
: 向列表尾部添加一个新的项insert(position, element)
: 向列表的特定位置插入一个新的项get(position)
: 获取对应位置的元素indexOf(element)
: 返回元素在列表中的索引。如果列表中没有该元素则返回-1update(position, element)
: 修改某个位置的元素removeAt(position)
: 从列表的特定位置移除一项remove(element)
: 从列表中移除一项isEmpty()
: 如果链表中不包含任何元素,返回true,如果链表长度大于0则返回falsesize()
: 返回链表包含的元素个数,与数组的length属性类似toString()
: 由于列表项使用了Node类,就需要重写继承自JS对象默认的toString方法,让其只输出元素的值forwardString()
: 返回正向遍历的节点字符串形式backwordString()
: 返回反向遍历的节点字符串形式append()方法
代码分析
newNode: this.tail.next = newNode
newNode.prev = this.tail
this.tail = newNode
即可// 常见的操作:方法
// 1. append方法
DoublyLinkedList.prototype.append = function (data) {
// 1. 根据data创建节点
var newNode = new Node(data)
// 2. 判断添加的是否是第一个节点
if (this.length == 0) {
this.head = newNode
this.tail = newNode
} else {
newNode.prev = this.tail
this.tail.next = newNode
this.tail = newNode
}
// 3. length+1
this.length += 1
}
toString()方法
forwardString()方法
backwardString()方法
// 2. 将链表转成字符串形式
// 2.1 toString方法
DoublyLinkedList.prototype.toString = function () {
return this.backwardString()
}
// 2.2 forwardString方法
DoublyLinkedList.prototype.forwardString = function () {
// 1. 定义变量
var current = this.tail
var resultString = ""
// 2. 依次向前遍历,获取每一个节点
while (current) {
resultString += current.data + " "
current = current.prev
}
return resultString
}
// 2.3 backwardString方法
DoublyLinkedList.prototype.backwardString = function () {
// 1. 定义变量
var current = this.head
var resultString = ""
// 2. 依次向后遍历,获取每一个节点
while (current) {
resultString += current.data + " "
current = current.next
}
return resultString
}
insert()方法
代码分析
情况一
: 将元素插入到头部
(position === 0)
情况二
: 将元素插入到尾部
(position === length)
情况三
: 将元素插入到中间位置
// 3. insert方法
DoublyLinkedList.prototype.insert = function (position, data) {
// 1. 越界判断
if (position < 0 || position > this.length) return false
// 2. 根据data创建新的节点
var newNode = new Node(data)
// 3. 判断原来的列表是否为空
if (this.length = 0) {
this.head = newNode
this.tail = newNode
} else {
if (position == 0) {
// 3.1 判断position是否为0
this.head.prev = newNode
newNode.next = this.head
this.head = newNode
} else if (position == this.length) {
// 3.2 position == length
newNode.prev = this.tail
this.tail.next = newNode
this.tail = newNode
} else {
// 3.3 其他情况
var current = this.head
var index = 0
while (index++ < position) {
current = current.next
}
// 修改指针
newNode.next = current
newNode.prev = current.prev
current.prev.next = newNode
current.prev = newNode
}
}
// 4. length+1
this.length += 1
return true
}
get()方法
// 4. get方法
DoublyLinkedList.prototype.get = function (position) {
// 1. 越界判断
if (position < 0 || position >= this.length) return null
// 2. 获取元素
var current = this.head
var index = 0
while (index++ < position) {
current = current.next
}
return current.data
}
indexOf()方法
// 5. indexOf方法
DoublyLinkedList.prototype.indexOf = function (data) {
// 1. 定义变量
var current = this.head
var index = 0
// 2. 查找和data相同的节点
while (current) {
if (current.data == data) {
return index
}
current = current.next
index += 1
}
return -1
}
update()方法
// 6. update方法
DoublyLinkedList.prototype.update = function (position, newData) {
// 1. 越界的判断
if (position < 0 || position >= this.length) return false
// 2. 寻找正确的节点
var current = this.head
var index = 0
while (index++ < position) {
current = current.next
}
// 3. 修改找到的节点的data信息
current.data = newData
return true
}
removeAt()方法
代码分析
// 7. removeAt方法
DoublyLinkedList.prototype.removeAt = function (position) {
// 1. 越界判断
if (position < 0 || position >= this.length) return null
// 2. 判断是否只有一个节点
var current = this.head
if (this.length == 1) {
this.head = null
this.tail = null
} else {
if (position == 0) {
// 判断是否删除的是第一个节点
this.head.next.prev = null
this.head = this.head.next
} else if (position == this.length - 1) {
// 最后节点
current = this.tail
this.tail.prev.next = null
this.tail = this.tail.prev
} else {
var index = 0
while (index++ < position) {
current = current.next
}
current.prev.next = current.next
current.next.prev = current.prev
}
}
// 3. length-1
this.length -= 1
return current.data
}
remove()方法
// 8. remove方法
DoublyLinkedList.prototype.remove = function (data) {
// 1.根据data获取下标值
var index = this.indexOf(data)
// 2.根据index删除对应位置的节点
return this.removeAt(index)
}
其他方法
// 9.其他方法
// isEmpty方法 判断是否为空
DoublyLinkedList.prototype.isEmpty = function () {
return this.length == 0
}
// size方法 获取链表长度
DoublyLinkedList.prototype.size = function () {
return this.length
}
// 获取链表的第一个元素
DoublyLinkedList.prototype.getHead = function () {
return this.head.data
}
// 获取链表的最后一个元素
DoublyLinkedList.prototype.getTail = function () {
return this.tail.data
}
集合特点
创建集合类
// 封装集合类
function Set() {
// 属性
this.items = {
}
// 集合的操作方法
}
集合常用的操作方法
add(value)
:向集合添加一个新的项remove(value)
:从集合移除一个值has(value)
:如果值在集合中,返回true,否则返回falseclear()
:移除集合中的所有项size()
:返回集合所包含元素的数量。与数组的length属性类似values()
:返回一个包含集合中所有值的数组has()方法
// has方法
Set.prototype.has = function (value) {
return this.items.hasOwnProperty(value)
}
add()方法
// add方法
Set.prototype.add = function (value) {
// 判断当前集合中是否已经包含了该元素
if (this.has(value)) {
return false
}
// 将元素添加到集合中
this.items[value] = value
return true
}
remove()方法
// remove方法
Set.prototype.remove = function (value) {
// 1. 判断该集合中是否包含该元素
if (!this.has(value)) {
return false
}
// 2. 将元素从属性中删除
delete this.items[value]
return true
}
clear()方法
// clear方法
Set.prototype.clear = function () {
this.items = {
}
}
size()方法
// size方法
Set.prototype.size = function () {
return Object.keys(this.items).length
}
values()方法
Set.prototype.values = function () {
return Object.keys(this.items)
}
并集
交集
差集
子集
并集
A∪B={x|x∈A,或x∈B}
代码解析
// 并集
Set.prototype.union = function (otherSet) {
// this: 集合对象A
// otherSet: 集合对象B
// 1. 创建新的集合
var unionSet = new Set()
// 2. 将A集合中所有的元素添加到新集合中
var values = this.values()
for (var i = 0; i < values.length; i++) {
unionSet.add(values[i])
}
// 3. 取出B集合中的元素,判断是否需要加到新集合
values = otherSet.values()
for (var i = 0; i < values.length; i++) {
unionSet.add(values[i])
}
return unionSet
}
交集
A∩B= {x|x∈A∧x∈B}
代码解析
// 交集
Set.prototype.intersection = function (otherSet) {
// this: 集合A
// otherSet: 集合B
// 1. 创建新的集合
var intersectionSet = new Set()
// 2. 从A中取出一个个元素,判断是否同时存在于集合B中,存在放入新集合中
var values = this.values()
for (var i = 0; i < values.length; i++) {
var item = values[i]
if (otherSet.has(item)) {
intersectionSet.add(item)
}
}
return intersectionSet
}
差集
A-B= {x|x∈A∧x∉B}
代码解析
// 差集
Set.prototype.difference = function (otherSet) {
// this: 集合A
// otherSet: 集合B
// 1. 创建新的集合
var differenceSet = new Set()
// 2. 取出A集合一个个元素,判断是否同时存在于B中,不存在B中,则添加到新集合中
var values = this.values()
for (var i = 0; i < values.length; i++) {
var item = values[i]
if (!otherSet.has(item)) {
differenceSet.add(item)
}
}
return differenceSet
}
子集
∀a∈A有a∈B,则A⊆B
代码解析
// 子集
Set.prototype.subset = function (otherSet) {
// this: 集合A
// otherSet: 集合B
// 遍历集合A中所有的元素,如果发现,集合A中的元素,在集合B中不存在,那么false
// 如果遍历完了整个集合,依然没有返回false,那么返回true即可
var values = this.values()
for (var i = 0; i < values.length; i++) {
var item = values[i]
if (!otherSet.has(item)) {
return false
}
}
return true
}
一一对应
的关系
数组
的方式: [18, “zs”, 1.88]. 可以通过下标值取出信息.字典
的方式: {“age” : 18, “name” : “zs”, “height”: 1.88}. 可以通过key取出value映射
关系为字典
(比如Swift中Dictionary
, Python中的dict
)映射
关系为Map
(比如Java中就有HashMap&TreeMap
等)代码解析
// 创建字典的构造函数
function Dictionay() {
// 字典属性
this.items = {
}
// 字典操作方法
}
set(key,value)
:向字典中添加新元素remove(key)
:通过使用键值来从字典中移除键值对应的数据值has(key)
:如果某个键值存在于这个字典中,则返回true,反之则返回falseget(key)
:通过键值查找特定的数值并返回clear()
:将这个字典中的所有元素全部删除size()
:返回字典所包含元素的数量。与数组的length属性类似keys()
:将字典所包含的所有键名以数组形式返回values()
:将字典所包含的所有数值以数组形式返回// 创建字典的构造函数
function Dictionay() {
// 字典属性
this.items = {
}
// 字典操作方法
// 在字典中添加键值对
Dictionay.prototype.set = function (key, value) {
this.items[key] = value
}
// 判断字典中是否有某个key
Dictionay.prototype.has = function (key) {
return this.items.hasOwnProperty(key)
}
// 从字典中移除元素
Dictionay.prototype.remove = function (key) {
// 1.判断字典中是否有这个key
if (!this.has(key)) return false
// 2.从字典中删除key
delete this.items[key]
return true
}
// 根据key去获取value
Dictionay.prototype.get = function (key) {
return this.has(key) ? this.items[key] : undefined
}
// 获取所有的keys
Dictionay.prototype.keys = function () {
return Object.keys(this.items)
}
// 获取所有的value
Dictionay.prototype.values = function () {
return Object.values(this.items)
}
// size方法
Dictionay.prototype.size = function () {
return this.keys().length
}
// clear方法
Dictionay.prototype.clear = function () {
this.items = {
}
}
}
基于数组
进行实现的优势
插入-删除-查找
操作缺点
没有顺序
的固定
的方式(比如从小到大)来遍历其中的元素不允许重复
的, 不能放置相同的key, 用于保存不同的元素什么是哈希表
哈希函数
, 通过哈希函数可以获取到HashCode
案例一
员工信息
方案
缺点
解决方案
案例二
联系人电话
方案
缺点
解决方案
案例三
单词信息
方案
案例四
高级语言的编辑器
如何将字符串转成数组的下标值
求和
问题
幂的连乘
来表示唯一性
问题
总结
压缩方法
取余操作符
取余操作的实现
index = largeNumber % smallRange
哈希化
哈希函数
哈希表
下标值重合
的情况
每个下标对应一个数据项
,但是通常情况下不可能解决方案
图解链地址法(拉链法)
方法解析
如何选择
线性查找
, 这个时候数组和链表的效率是差不多的寻找空白的单元格来添加重复的数据
图解开放地址法
问题
开放地址法解决方案
线性探测
二次探测
再哈希法
线性探测: 线性的查找空白的单元
插入32
index=2
index位置+1
开始一点点查找合适的位置来放置32index=3
的位置为空, 32就会放在该位置查询32
index=2
index位置+1
开始查找和32一样的注意:查询到空位置, 就停止
删除32
线性探测的问题
聚集
聚集
优化
:步长
步长为1
的探测
从下标值x开始
, 那么线性测试就是x+1, x+2, x+3
依次探测从下标值x开始, x+1², x+2², x+3²
二次探测的问题
步长不一
的一种聚集
. 还是会影响效率步长+1
还是步长+平法
中存在的问题再哈希法
再哈希法
步长是固定
的: 1, 4, 9, 16, 依次类推.依赖关键字
的探测序列, 而不是每个关键字都一样再哈希法的做法
再做一次哈希化
, 用这次哈希化的结果作为步长不变
的, 不过不同的关键字使用不同的步长第二次哈希化特点
和第一个哈希函数不同
不能输出为0
哈希函数
stepSize = constant - (key - constant)
constant是质数
, 且小于数组的容量stepSize = 5 - (key % 5)
, 满足需求, 并且结果不可能为0O(1)
的时间级,效率非常高
没有发生冲突
成正比
常量时间
填装因子
填装因子变大,探测长度也越来越长
装填因子
数据项
和整个哈希表长度
的比值
装填因子 = 总数据项 / 哈希表长度
无限的延伸
下去(后面效率就变低了)线性探测
探测序列(P)
和填装因子(L)
的关系
成功
的查找: P = (1+1/(1-L))/2
不成功
的查找: P=(1+1/(1-L)^2)/2
图解算法效率
图片解析
填装因子是1/2
时
1.5
次比较2.5
次填装因子为2/3
时
2.0
次比较5.0
次如果填装因子更大,比较次数会非常大
存储效率
和速度
之间的平衡
填装因子变小,存储效率下降,而速度上升
===========================================================
二次探测和再哈希
成功
的搜索,公式是: -log2(1 - loadFactor) / loadFactor
不成功
的搜搜, 公式是: 1 / (1-loadFactor)
图解算法效率
图片解析
填装因子是0.5
时
2次比较
填装因子为2/3
时
2.37
和3.0
次比较填装因子为0.8
时
2.9
和5.0
次较高
的填装因子,对比线性探测,二次探测和再哈希法还是可以忍受的arraySize
个数据项, 每个数据项有一个链表, 在表中一共包含N
个数据项N / arraySize
装填因子
成功
可能只需要查找链表的一半
即可: 1 + loadFactor/2
不成功
可能需要将整个链表查询完才知道不成功: 1 + loadFactor
图解算法效率
Java的HashMap
中使用的就是链地址法快速的计算
简单
, 应该可以快速计算出结果
哈希表的主要优点
速度
尽量少的有乘法和除法
乘除的性能是比较低
cats = 3*27³+1*27²+20*27+19= 60337
直观的计算结果
, 可能不止4项, 可能有更多项a(n)xn+a(n-1)x(n-1)+…+a(1)x+a(0)
n+(n-1)+…+1=n(n+1)/2
n次
霍纳法则
Pn(x)= anx n+a(n-1)x(n-1)+…+a1x+a0=((…(((anx +an-1)x+an-2)x+ an-3)…)x+a1)x+a0
N次
N次
均匀分布
均匀的分布
链地址法
开放地址法
质数的使用
哈希表的长度
N次幂的底数
哈希表的长度使用质数
再哈希法中质数的重要性
不是质数
, 例如: 表长为15(下标值0~14)映射到0, 步长为5
探测序列是多少呢?依次类推, 循环下去
一直循环
下去, 直到程序崩溃是一个质数
, 比如13. 探测序列是多少呢?不会产生循环
, 而且可以让数据在哈希表中更加均匀
的分布没有那么重要
链地址法
HashMap
的初始长度是16, 每次自动扩展
, 长度必须是2的次幂
从Key映射到index
的算法HashMap
中为了提高效率
, 采用了位运算
的方式
index = HashCode(Key) & (Length - 1)
hashcode
,结果为十进制的3029737
,二进制的101110001110101110 1001
与运算
,101110001110101110 1001 & 1111 = 1001
,十进制是9
,所以 index=9
较大数据的位运算
时会出问题, 所以代码实现中还是使用了取模
N次幂的底数, 使用质数
为了产生的数据不按照某种规律递增
按照4进行递增
: 0 4 8 12 16, 将其映射到程度为8的哈希表中本身不是质数
, 而递增的数量可以使用质数
, 比如5, 那么 0 5 10 15 20代码实现
// 设计哈希函数
// 1. 将字符串转成比较大的数字: hashCode
// 2. 将大的数字 hashCode 压缩到数组范围(大小)之内
function hashFunc(str, size) {
// 1. 定义 hashCode
var hashCode = 0
// 2. 霍纳算法,来计算 hashCode 的值
// cats -> Unicode编码
for (var i = 0; i < str.length; i++) {
hashCode = 37 * hashCode + str.charCodeAt(i)
}
// 取余操作
var index = hashCode % size
return index
}
// 测试哈希函数
alert(hashFunc('abc', 7)) // 4
alert(hashFunc('cba', 7)) // 3
alert(hashFunc('nba', 7)) // 5
alert(hashFunc('mba', 7)) // 1
链地址法
来实现哈希表
基于storage的数组
)每个index
对应的是一个数组(bucket
)bucket
中存放key
和value
, 继续使用一个数组
[[ [k,v], [k,v], [k,v] ] , [ [k,v], [k,v] ], [ [k,v] ] ]
创建哈希表
HashTable
// 封装哈希表类
function HashTable() {
// 属性
this.storage = []
this.count = 0
this.limit = 7
// 方法
// 哈希函数
HashTable.prototype.hashFunc = function (str, size) {
// 1. 定义 hashCode 变量
var hashCode = 0
// 2. 霍纳算法,来计算 hashCode 的值
// cats -> Unicode 编码
for (var i = 0; i < str.length; i++) {
hashCode = 37 * hashCode + str.charCodeAt(i)
}
// 3. 取模操作
var index = hashCode % size
return index
}
}
代码解析
storage
作为数组
, 数组中存放相关的元素count
表示当前已经存在了多少数据
limit
用于标记数组中一共可以存放多少个元素
HashTable
中HashTable.prototype.put = function (key, value) {
// 1. 根据 key 获取对应的 index
var index = this.hashFunc(key, this.limit)
// 2. 根据 index 取出对应的 bucket
var bucket = this.storage[index]
// 3. 判断该 bucket 是否为 null
if (bucket == null) {
bucket = []
this.storage[index] = bucket
}
// 4. 判断是否是修改数据
for (var i = 0; i < bucket.length; i++) {
var tuple = bucket[i]
if (tuple[0] == key) {
tuple[1] == value
return
}
}
// 5. 进行添加操作
bucket.push([key, value])
this.count += 1
}
代码解析
key
获取对应的hashCode
, 也就是数组的index
index
位置中取出另外一个数组bucket
是否为null
null
, 表示之前在该位置没有放置过任何的内容, 那么就新建一个数组[]
key
对应的value
依次替换
操作, 而不是插入新的数据变量override
来记录是否是修改操作插入新的数据
bucket
中push
新的[key, value]
即可count+1
, 因为数据增加了一项根据key获取value
HashTable.prototype.get = function (key) {
// 1. 根据 key 获取对应的 index
var index = this.hashFunc(key, this.limit)
// 2. 根据 index 获取对应的 bucket
var bucket = this.storage[index]
// 3. 判断 bucket 是否为 null
if (bucket == null) {
return null
}
// 4. 有 bucket,那么就进行线性查找
for (var i = 0; i < bucket.length; i++) {
var tuple = bucket[i]
if (tuple[0] === key) {
return tuple[1]
}
}
// 5. 依然没有找到,那么返回 null
return null
}
代码解析
key
获取hashCode
(也就是index
)index
取出bucket
bucket
都是null
, 那么说明这个位置之前并没有插入过数据
bucket
, 就遍历
, 并且如果找到, 就将对应的value
返回即可null
key
, 删除对应的key/value
HashTable.prototype.remove = function (key) {
// 1. 根据 key 获取对应的 index
var index = this.hashFunc(key, this.limit)
// 2. 根据 index 获取对应的 bucket
var bucket = this.storage[index]
// 3. 判断 bucket 是否为 null
if (bucket == null) return null
// 4. 有 bucket,那么就进行线性查找,并且删除
for (var i = 0; i < bucket.length; i++) {
var tuple = bucket[i]
if (tuple[0] == key) {
bucket.splice(i, 1)
this.count--
return tuple[1]
}
}
// 5. 依然没有找到,那么返回 null
return null
}
代码思路
key
获取对应的 index
index
获取 bucket
bucket
是否存在,如果不存在,那么直接返回 null
bucket
,寻找对应的数据,并且删除null
为什么需要扩容
链地址法
, loadFactor
可以大于1, 所以这个哈希表可以无限制
的插入新数据数据量的增多
, 每一个index
对应的bucket
会越来越长, 也就造成效率的降低
扩容
. 比如扩容两倍如何进行扩容
将容量增加大两倍
同时进行修改
(重新哈希化, 来获取到不同的位置)耗时
的过程, 但是如果数组需要扩容, 那么这个过程是必要的什么情况下扩容呢
loadFactor>0.75
的时候进行扩容// 哈希表扩容/缩容
HashTable.prototype.resize = function (newLimit) {
// 1. 保存旧的数组内存
var oldStorage = this.storage
// 2. 重置所有的属性
this.storage = []
this.count = 0
this.limit = newLimit
// 3. 遍历 oldStorage 中所有的 bucket
for (var i = 0; i < oldStorage.length; i++) {
// 3.1 取出对应的 bucket
var bucket = oldStorage[i]
// 3.2 判断 bucket 是否为 null
if (bucket == null) {
continue
}
// 3.3 bucket 中有数据,那么取出数据,重新插入
for (var i = 0; i < bucket.length; i++) {
var tuple = bucket[i]
this.put(tuple[0], tuple[i])
}
}
}
代码解析
storeage = []
重置
重新插入
到哈希表中在什么时候调用
扩容方法
判断
修改put方法
HashTable.prototype.put = function (key, value) {
// 1. 根据 key 获取对应的 index
var index = this.hashFunc(key, this.limit)
// 2. 根据 index 取出对应的 bucket
var bucket = this.storage[index]
// 3. 判断该 bucket 是否为 null
if (bucket == null) {
bucket = []
this.storage[index] = bucket
}
// 4. 判断是否是修改数据
for (var i = 0; i < bucket.length; i++) {
var tuple = bucket[i]
if (tuple[0] == key) {
tuple[1] = value
return
}
}
// 5. 进行添加操作
bucket.push([key, value])
this.count += 1
// 6. 判断是否需要扩容操作
if (this.count > this.limit * 0.75) {
this.resize(this.limit * 2)
}
}
不断的删除数据
loadFactor < 0.25
的时候, 最好将数量限制在一半
修改remove方法
HashTable.prototype.remove = function (key) {
// 1. 根据 key 获取对应的 index
var index = this.hashFunc(key, this.limit)
// 2. 根据 index 获取对应的 bucket
var bucket = this.storage[index]
// 3. 判断 bucket 是否为 null
if (bucket == null) return null
// 4. 有 bucket,那么就进行线性查找,并且删除
for (var i = 0; i < bucket.length; i++) {
var tuple = bucket[i]
if (tuple[0] == key) {
bucket.splice(i, 1)
this.count--
return tuple[1]
// 缩小容量
if (this.limit > 7 && this.count < this.limit * 0.25) {
this.resize(Math.floor(this.limit / 2))
}
}
}
质数的特点
function isPrime(num) {
for (var i = 2; i < num; i++) {
if (num % i == 0) {
return false
}
}
return true
}
// 测试
alert(isPrime(3)) // true
alert(isPrime(32)) // false
alert(isPrime(37)) // true
效率不高
function isPrime(num) {
// 1.获取平方根
var temp = parseInt(Math.sqrt(num))
// 2.循环判断
for (var i = 2; i <= temp; i++) {
if (num % i == 0) {
return false
}
}
return true
}
原来的容量 x 2
封装获取新的容量的代码(质数)
// 判断某个数字是否是质数
HashTable.prototype.isPrime = function (num) {
// 1. 获取 num 的平方根
var temp = parseInt(Math.sqrt(num))
// 2. 循环判断
for (var i = 2; i < temp; i++) {
if (num % i == 0) {
return false
}
}
return true
}
// 获取质数的方法
HashTable.prototype.getPrime = function (num) {
while (!this.isPrime(num)) {
num++
}
return num
}
修改插入数据的代码
// 6. 判断是否需要扩容操作
if (this.count > this.limit * 0.75) {
var newSize = this.limit * 2
var newPrime = this.getPrime(newSize)
this.resize(newPrime)
}
修改删除数据的代码
// 缩小容量
if (this.limit > 7 && this.count < this.limit * 0.25) {
var newSize = Math.floor(this.limit / 2)
var newPrime = this.getPrime(newSize)
this.resize(newPrime))
}
简介
树结构图
树的定义
注意
:
数组
优点
缺点
链表
优点
缺点
哈希表
优点
缺点
树结构
树的术语
结点的度(Degree)
:结点的子树个数树的度
:树的所有结点中最大的度数. (树的度通常为结点的个数N-1)叶结点(Leaf)
:度为0的结点. (也称为叶子结点)父结点(Parent)
:有子树的结点是其子树的根结点的父结点子结点(Child)
:若A结点是B结点的父结点,则称B结点是A结点的子结点;子结点也称孩子结点兄弟结点(Sibling)
:具有同一父结点的各结点彼此是兄弟结点路径和路径长度
:从结点n1到nk的路径为一个结点序列n1 , n2,… , nk, ni是 ni+1的父结点。路径所包含边的个数为路径的长度结点的层次(Level)
:规定根结点在1层,其它任一结点的层数是其父结点的层数加1树的深度(Depth)
:树中所有结点中的最大层次是这棵树的深度最多只能有两个子节点
, 这样的树就成为"二叉树
"完美二叉树(Perfect Binary Tree)
, 也称为满二叉树(Full Binary Tree)
完全二叉树(Complete Binary Tree)
二叉树的存储常见的方式是数组
和链表
数组存储
非完全二叉树
链表存储
什么是二叉搜索树?
二叉搜索树的特点
insert(key)
:向树中插入一个新的键search(key)
:在树中查找一个键,如果结点存在,则返回true;如果不存在,则返回falseinOrderTraverse
:通过中序遍历方式遍历所有结点preOrderTraverse
:通过先序遍历方式遍历所有结点postOrderTraverse
:通过后序遍历方式遍历所有结点min
:返回树中最小的值/键max
:返回树中最大的值/键remove(key)
:从树中移除某个键代码解析
BinarySerachTree
的构造函数代码实现
// 创建 BinarySerachTree
function BinarySerachTree() {
// 创建结点构造函数
function Node(key) {
this.key = key
this.left = null
this.right = null
}
// 保存根的属性
this.root = null
// 二叉搜索树相关的操作方法
}
外界调用的 insert 方法
// 插入数据
BinarySerachTree.prototype.insert = function (key) {
// 1. 根据 key 创建对应的 node
var newNode = new Node(key)
// 2. 判断根结点是否有值
if(this.root === null) {
this.root = newNode
} else {
this.insertNode(this.root, newNode)
}
}
代码解析
插入非根结点
BinarySerachTree.prototype.insertNode = function (node, newNode) {
if (newNode.key < node.key) {
// 1. 准备向左子树插入数据
if (node.left === null) {
// 1.1 node 的左子树上没有内容
node.left = newNode // 数据插入的结点位置
} else {
// 1.2 node 的左子树上已经有了内容
this.insertNode(node.left, newNode) // 递归调用
}
} else {
// 2. 准备向右子树插入数据
if (node.right === null) {
// 2.1 node 的右子树上没有内容
node.right = newNode // 数据插入的结点位置
} else {
// 2.2 node 的右子树上有内容
this.insertNode(node.right, newNode) // 递归调用
}
}
}
代码解析
树的遍历
先序遍历/中序遍历/后序遍历
. (还有层序遍历, 使用较少, 可以使用队列来完成)遍历过程
代码实现
BinarySerachTree.prototype.preOrderTraversal = function (handler) {
this.preOrderTranversalNode(this.root, handler)
}
BinarySerachTree.prototype.preOrderTranversalNode = function (node, handler) {
if (node !== null) {
// 1. 打印当前经过的节点
handler(node.key)
// 2. 遍历所有的左子树
this.preOrderTranversalNode(node.left, handler)
// 3. 遍历所有的右子树
this.preOrderTranversalNode(node.right, handler)
}
}
// 测试代码
var resulting = ""
bst.preOrderTraversal(function (key) {
resulting += key + ""
})
alert(resulting)
代码解析
// 中序遍历
BinarySerachTree.prototype.midOrderTraversal = function (handler) {
this.midOrderTraversalNode(this.root, handler)
}
BinarySerachTree.prototype.midOrderTraversalNode = function (node, handler) {
if (node != null) {
// 1. 处理左子树中的节点
this.midOrderTraversalNode(node.left, handler)
// 2. 处理节点
handler(node.key)
// 3. 处理右子树中的节点
this.midOrderTraversalNode(node.right, handler)
}
}
// 后序遍历
BinarySerachTree.prototype.postOrderTraversal = function (handler) {
this.postOrderTraversalNode(this.root, handler)
}
BinarySerachTree.prototype.postOrderTraversalNode = function (node, handler) {
if (node != null) {
// 1. 查看左子树中的节点
this.postOrderTraversalNode(node.left, handler)
// 2. 查找右子树中节点
this.postOrderTraversalNode(node.right, handler)
// 3. 处理节点
handler(node.key)
}
}
代码实现
// 获取最值
// 最大值
BinarySerachTree.prototype.max = function () {
// 1. 获取根节点
var node = this.root
// 2. 依次向右不断的查找,直到节点为null
var key = null
while (node !== null) {
key = node.key
node = node.right
}
return key
}
// 最小值
BinarySerachTree.prototype.min = function () {
// 1. 获取根节点
var node = this.root
// 2. 依次向左不断的查找,直到节点为null
var key = null
while (node != null) {
key = node.key
node = node.left
}
return key
}
代码解析
非递归代码实现
// 3. 搜索某一个 key
BinarySerachTree.prototype.search = function (key) {
// 1. 获取根节点
var node = this.root
// 2. 循环搜索 key
while (node != null) {
if (key < node.key) {
node = node.left
} else if (key > node.key) {
node = node.right
} else {
return true
}
}
return false
}
递归代码实现
// 搜索特定的值
// 递归方法
BinarySerachTree.prototype.search = function (key) {
return this.searchNode(this.root, key)
}
BinaryserachTree.prptotype.searchNode = function (node, key) {
// 1. 如果传入的 node 为 null,那么就退出递归
if (node === null) {
return false
}
// 2. 判断 node 节点的值和传入的 key 大小
if (node.key > key) {
// 2.1 传入的 key 较小,向左边继续查找
return this.searchNode(node.left, key)
} else if (node.key < key) {
// 2.2 传入的 key 较大,向右边继续查找
return this.searchNode(node.right, key)
} else {
// 2.3 相同,说明找到了 key
return true
}
}
代码解析
退出条件
, 这里是两种情况下退出
node === null
, 也就是后面不再有节点的时候node.key === key
的时候node.key > key
, 那么说明传入的值更小, 需要向左查找node.key < key
, 那么说明传入的值更大, 需要向右查找递归or循环?
简化代码
, 但是也会增加空间的复杂度
三种情况
:
叶结点
(没有字节点, 比较简单)一个子节点
(也相对简单)两个子节点
.(情况比较复杂)先从查找要删除的节点
// 二叉树的删除
BinarySerachTree.prototype.remove = function (key) {
// 寻找要删除的节点
// 1. 定义变量,保存一些信息
var current = this.root
var parent = null
var isLeftChild = true
// 2. 开始寻找删除的节点
while (current.key != key) {
parent = current
if (key < current.key) {
isLeftChild = true
current = current.left
} else {
isLeftChild = false
current = current.right
}
// 某种情况: 已经找到了最后的节点,依然没有找到 ==key
if (current == null) return false
}
// 2. 根据对应的情况删除节点
}
代码解析
先保存了一些临时变量
current
: 用于一会儿找到的要删除的节点对应的nodeparent
: 用于保存current节点的父节点. 因为如果current有子节点, 那么在删除current节点的时候, 必然需要将parent的left或者right指向它的某一个子节点. 所以需要保存起来current的parent. (树中的节点关系不能向上的, 和链表非常相似)isLeftChild
: boolean类型,它用户记录我们是在current是在父节点的左侧还是右侧, 以便到时候设置parent的left或者right之后开始查找对应的key
current/parent/isLeftChild
这些变量current === null
, 那么说明在二叉搜索树中没有该key, 直接返回false即可情况一: 没有子节点
代码实现
// 情况一
if (current.left == null && current.right == null) {
if (current == this.root) {
this.root = null
} else if (isLeftChild) {
parent.left = null
} else {
parent.right = null
}
}
代码解析
图解
情况二: 有一个子节点
代码实现
// 删除有一个子节点的节点
else if (current.right == null) {
if (current == this.root) {
this.root = current.left
} else if (isLeftChild) {
parent.left = current.left
} else {
parent.right = current.left
}
} else if (current.left == null) {
if (current == this.root) {
this.root = current.right
} else if (isLeftChild) {
parent.left = current.right
} else {
parent.right = current.right
}
}
代码解析
图解
问题
情况一: 删除9节点
情况二: 删除7节点
情况三: 删除15节点,并且也在右边找
删除规律
寻找后继代码实现
// 找后继的方法
BinarySerachTree.prototype.getSuccessor = function (delNode) {
// 1. 定义变量,保存找到的后继
var successor = delNode
var current = delNode.right
var successorParent = delNode
// 2. 循环查找
while (current != null) {
successorParent = successor
successor = current
current = current.left
}
// 3. 判断寻找的后继节点是否直接就是 delNode 的 right 节点
if (successor != delNode.right) {
successorParent.left = successor.right
successor.right = delNode.right
}
return successor
}
找到后继后的处理代码
// 2.3 删除的节点有两个子节点
else {
// 1. 获取后继节点
var successor = this.getSuccessor(current)
// 2. 判断是否是根节点
if (current == this.root) {
this.root = successor
} else if (isLeftChild) {
parent.left = successor
} else {
parent.right = successor
}
// 3. 将删除节点的左子树 = current.left
successor.left = current.left
}
代码解析
情况一: 是根节点
,那么 this.root = successor
. 并且 successor 的 left 应该等于 current 的 left情况二: 是父节点的左节点
,parent.left = successor
,并且 successor 的 left 应该等于 current 的 left情况三: 是父节点的右节点
,parent.right = successor
,并且 successor 的 left 应该等于 current 的 leftsuccessor.left = current.left
从判断中抽取出来如何删除15?
successorParent.left = successor.right
successor.right = delNode.right
删除节点完整代码
// 二叉树的删除
BinarySerachTree.prototype.remove = function (key) {
// 1. 寻找要删除的节点
// 1.1 定义变量,保存一些信息
var current = this.root
var parent = null
var isLeftChild = true
// 1.2 开始寻找删除的节点
while (current.key !== key) {
parent = current
if (key < current.key) {
isLeftChild = true
current = current.left
} else {
isLeftChild = false
current = current.right
}
// 某种情况: 已经找到了最后的节点,依然没有找到 ==key
if (current === null) return false
}
// 2. 根据对应的情况删除节点
// 找到了 current.key == key
// 2.1 删除的节点是叶子节点(没有子节点)
if (current.left === null && current.right === null) {
if (current == this.root) {
this.root = null
} else if (isLeftChild) {
parent.left = null
} else {
parent.right = null
}
}
// 2.2 删除的节点有一个子节点
else if (current.right === null) {
if (current == this.root) {
this.root = current.left
} else if (isLeftChild) {
parent.left = current.left
} else {
parent.right = current.left
}
} else if (current.left === null) {
if (current == this.root) {
this.root = current.right
} else if (isLeftChild) {
parent.left = current.right
} else {
parent.right = current.right
}
}
// 2.3 删除的节点有两个子节点
else {
// 1. 获取后继节点
var successor = this.getSuccessor(current)
// 2. 判断是否是根节点
if (current == this.root) {
this.root = successor
} else if (isLeftChild) {
parent.left = successor
} else {
parent.right = successor
}
// 3. 将删除节点的左子树 = current.left
successor.left = current.left
}
return true
}
// 找后继的方法
BinarySerachTree.prototype.getSuccessor = function (delNode) {
// 1. 定义变量,保存找到的后继
var successor = delNode
var current = delNode.right
var successorParent = delNode
// 2. 循环查找
while (current != null) {
successorParent = successor
successor = current
current = current.left
}
// 3. 判断寻找的后继节点是否直接就是 delNode 的 right 节点
if (successor != delNode.right) {
successorParent.left = successor.right
successor.right = delNode.right
}
return successor
}
如何避开节点删除操作
操作缺陷
// 封装二叉搜索树
function BinarySerachTree() {
function Node(key) {
this.key = key
this.left = null
this.right = null
}
// 属性
this.root = null
// 方法
// 插入数据: 对外给用户调用的方法
BinarySerachTree.prototype.insert = function (key) {
// 1. 根据 key 创建节点
var newNode = new Node(key)
// 2. 判断根节点是否有值
if (this.root == null) {
this.root = newNode
} else {
this.insertNode(this.root, newNode)
}
}
BinarySerachTree.prototype.insertNode = function (node, newNode) {
if (newNode.key < node.key) {
if (node.left == null) {
node.left = newNode
} else {
this.insertNode(node.left, newNode)
}
} else {
if (node.right == null) {
node.right = newNode
} else {
this.insertNode(node.right, newNode)
}
}
}
// 树的遍历
// 1. 先序遍历
BinarySerachTree.prototype.preOrderTraversal = function (handler) {
this.preOrderTraversalNode(this.root, handler)
}
// 第一次: node -> 11
// 第二次: node -> 7
// 第三次: node -> 5
// 第四次: node -> 3
// 第四次: 3 -> right -> null -> 返回上层
// 第五次: node -> null
BinarySerachTree.prototype.preOrderTraversalNode = function (node, handler) {
if (node != null) {
// 1. 处理经过的节点
handler(node.key)
// 2. 处理经过节点的左子节点
this.preOrderTraversalNode(node.left, handler)
// 3. 处理经过节点的右子节点
this.preOrderTraversalNode(node.right, handler)
}
}
// 二叉树的删除
BinarySerachTree.prototype.remove = function (key) {
// 1. 寻找要删除的节点
// 1.1 定义变量,保存一些信息
var current = this.root
var parent = null
var isLeftChild = true
// 1.2 开始寻找删除的节点
while (current.key !== key) {
parent = current
if (key < current.key) {
isLeftChild = true
current = current.left
} else {
isLeftChild = false
current = current.right
}
// 某种情况: 已经找到了最后的节点,依然没有找到 ==key
if (current === null) return false
}
// 2. 根据对应的情况删除节点
// 找到了 current.key == key
// 2.1 删除的节点是叶子节点(没有子节点)
if (current.left === null && current.right === null) {
if (current == this.root) {
this.root = null
} else if (isLeftChild) {
parent.left = null
} else {
parent.right = null
}
}
// 2.2 删除的节点有一个子节点
else if (current.right === null) {
if (current == this.root) {
this.root = current.left
} else if (isLeftChild) {
parent.left = current.left
} else {
parent.right = current.left
}
} else if (current.left === null) {
if (current == this.root) {
this.root = current.right
} else if (isLeftChild) {
parent.left = current.right
} else {
parent.right = current.right
}
}
// 2.3 删除的节点有两个子节点
else {
// 1. 获取后继节点
var successor = this.getSuccessor(current)
// 2. 判断是否是根节点
if (current == this.root) {
this.root = successor
} else if (isLeftChild) {
parent.left = successor
} else {
parent.right = successor
}
// 3. 将删除节点的左子树 = current.left
successor.left = current.left
}
return true
}
// 找后继的方法
BinarySerachTree.prototype.getSuccessor = function (delNode) {
// 1. 定义变量,保存找到的后继
var successor = delNode
var current = delNode.right
var successorParent = delNode
// 2. 循环查找
while (current != null) {
successorParent = successor
successor = current
current = current.left
}
// 3. 判断寻找的后继节点是否直接就是 delNode 的 right 节点
if (successor != delNode.right) {
successorParent.left = successor.right
successor.right = delNode.right
}
return successor
}
}
二叉搜索树的缺陷
非平衡树
左右分布均匀
的连续数据
后,分布的不均匀
,这种树为非平衡树
平衡二叉树
来说,插入/查找等操作的效率是O(logN)
非平衡二叉树
,相当于编写了一个链表,查找效率变成了O(N)
树的平衡性
较快的时间O(logN)
来操作一颗树,需要保证树总是平衡
的
每个节点左边的子孙节点
的个数,应该尽可能的等于右边的子孙节点的个数
常见的平衡树
AVL 树
树的平衡
(每个节点多存储了一个额外的数据)平衡
的,所以时间复杂度也是 O(logN)整体效率不如红黑树
红黑树
一些特性
来保持树的平衡特性
最长可能路径
,不会超过最短可能路径的两倍长
基本
是平衡的性质4: 每个红色节点的两个子节点都是黑色(从每个叶子到根的所有路径上不能有两个连续的红色节点)
性质5: 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点
换色 - 左旋转 - 右旋转
变色
红色
节点变为黑色
,或者把黑色
节点变为红色
新的节点
通常都是红色
节点
左旋转
右旋转
旋转过程中,节点的子树不会受到影响
节点为N
,其父节点为P
祖父节点为G
,其父亲的兄弟节点为U
(即P和U是同一个节点的子节点)具体情况
变化情况
操作方案
具体情况
变化情况
操作方案
可能出现的问题
具体情况
变化情况
操作方案
右旋转
具体情况
变化情况
操作结果
依次插入 10 9 8 7 6 5 4 3 2 1
插入 10
问题
变化
插入 9
问题
变化
插入 8
问题
变化
变色
二次变化
插入 7
问题
变化
变色
二次变化
插入 6
问题
变化
插入 5
问题
变化
插入 4
问题
变化
变色
二次变化
插入 3
问题
变化
变色
二次变化
变色
三次变化
插入 2
问题
变化
变色
二次变化
插入 1
问题
变化
变色
二次变化
变色
三次变化
变色
图的介绍
图的特点
顶点
边
相邻顶点
度
路径
无向图
有向图
无权图
带权图
一种比较常见的表示图的方式: 邻接矩阵
解析
邻接矩阵的问题
另外一种常用的表示图的方式: 邻接表
解析
邻接表的问题
(出度: 指向别人的数量, 入度: 指向自己的数量)
代码解析
代码实现
// 封装图结构
function Graph() {
// 属性: 顶点(数组)/边(字典)
this.vertexes = [] // 顶点
this.edges = new Dictionay() // 边
// 方法
}
添加方法
代码解析
代码实现
// 方法
// 添加方法
// 1. 添加顶点的方法
Graph.prototype.addVertex = function (v) {
this.vertexes.push(v)
this.edges.set(v, [])
}
// 2. 添加边的方法
Graph.prototype.addEdge = function (v1, v2) {
this.edges.get(v1).push(v2) // v1 -> v2
this.edges.get(v2).push(v1) // v2 -> v1
}
测试代码
// 测试代码
// 1. 创建图结构
var graph = new Graph()
// 2. 添加顶点
var myVertexes = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I']
for (var i = 0; i < myVertexes.length; i++) {
g.addVertex(myVertexes[i])
}
// 3. 添加边
// 添加边
graph.addEdge('A', 'B');
graph.addEdge('A', 'C');
graph.addEdge('A', 'D');
graph.addEdge('C', 'D');
graph.addEdge('C', 'G');
graph.addEdge('D', 'G');
graph.addEdge('D', 'H');
graph.addEdge('B', 'E');
graph.addEdge('B', 'F');
graph.addEdge('E', 'I');
// 实现 toString 方法
Graph.prototype.toString = function () {
// 1. 定义字符串,保存最终的结果
var resultString = ''
// 2. 遍历所有的顶点,以及顶点对应的边
for (var i = 0; i < this.vertexes.length; i++) {
resultString += this.vertexes[i] + '->'
var vEdges = this.edges.get(this.vertexes[i])
for (var j = 0; j < vEdges.length; j++) {
resultString += vEdges[j] + ' '
}
resultString += '\n'
}
return resultString
}
图的遍历思想
遍历的注意点
两种算法的思想
初始化代码
// 初始化状态颜色
Graph.prototype.initializeColor = function () {
var colors = []
for (var i = 0; i < this.vertexes.length; i++) {
colors[this.vertexes[i]] = 'while'
}
return colors
}
广度优先搜索算法的思路
广度优先搜索的实现
代码解析
代码实现
// 实现广度优先搜索(BFS)
Graph.prototype.bfs = function (initV, handler) {
// 1. 初始化颜色
var colors = this.initializeColor()
// 2. 创建队列
var queue = new Queue()
// 3. 将顶点加入到队列中
queue.enqueue(initV)
// 4. 循环从队列中取出元素
while (!queue.isEmpty()) {
// 4.1 从队列取出一个顶点
var v = queue.dequeue()
// 4.2 获取和顶点相连的另外顶点
var vList = this.edges.get(v)
// 4.3 将v的颜色设置成灰色
colors[v] = 'gray'
// 4.4 遍历所有的顶点,并且加入到队列中
for (var i = 0; i < vList.length; i++) {
var e = vList[i]
if (colors[e] == 'while') {
colors[e] = 'gray'
queue.enqueue(e)
}
}
// 4.5 访问顶点
handler(v)
// 4.6 将顶点设置为黑色
colors[v] = 'black'
}
}
// 5. 测试bfs
var result = ''
graph.bfs(graph.vertexes[0], function (v) {
result += v + ' '
})
alert(result)
深度优先搜索的思路
图解DFS
代码解析
代码实现
// 深度优先搜索(DFS)
Graph.prototype.dfs = function (initV, handler) {
// 1. 初始化颜色
var colors = this.initializeColor()
// 2. 从某个顶点开始依次递归访问
this.dfsVisit(initV, colors, handler)
}
Graph.prototype.dfsVisit = function (v, colors, handler) {
// 1. 将颜色设置为灰色
colors[v] = 'gray'
// 2. 处理v顶点
handler(v)
// 3. 访问v相连的顶点
var vList = this.edges.get(v)
for (var i = 0; i < vList.length; i++) {
var e = vList[i]
if (colors[e] == 'white') {
return this.dfsVisit(e, colors, handler)
}
}
// 4. 将v设置成黑色
colors[v] = 'black'
}
// 6. 测试dfs
result = ''
graph.dfs(graph.vertexes[0], function (v) {
result += v + ' '
})
alert(result)
粗略的度量
被称作大O表示法
数据项个数
发生变化时,算法的效率
会跟着发生改变算法的速度
会如何跟随着数据量的变化
的符号 | 名称 |
---|---|
O(1) | 常数 |
O(log(n)) | 对数 |
O(n) | 线性 |
O(nlog(n)) | 线性和对数乘积 |
O(n²) | 平方 |
O(2ⁿ) | 指数 |
推导大O表示法的方式
:
某个数据结构
中存储起来后(比如数组), 就可能根据需求对数据进行不同方式的排序冒泡排序
选择排序
插入排序
归并排序
计数排序(counting sort)
基数排序(radix sort)
希尔排序
堆排序
桶排序
计算机如何排序
计算机排序的特点
简单算法的主要操作
// 创建列表类
function ArrayList() {
// 属性
this.array = []
// 方法
// 将数据可以插入到数组中的方法
ArrayList.prototype.insert = function (item) {
this.array.push(item)
}
// toString
ArrayList.prototype.toString = function () {
return this.array.join('-')
}
}
运行效率较低
, 但是在概念上它是排序算法中最简单
的冒泡排序的思路
冒泡排序图解
冒泡排序的代码分析
冒泡排序的代码实现
// 冒泡排序
ArrayList.prototype.bubblesort = function () {
// 1. 获取数组的长度
var length = this.array.length
// 第一次: j = length - 1, 比较到倒数第一个位置
// 第二次: j = length - 2, 比较到倒数第二个位置
// ...
for (var j = length - 1; j >= 0; j--) {
// 第一次进来: i = 0, 比较 0 和 1 位置的两个数据,如果 0 位置大有 1 位置的数据
// 最后一次进来: i = length - 2,比较 length - 2 和 length - 1 的两个数据
for (var i = 0; i < j; i++) {
if (this.array[i] > this.array[i+1]) {
// 交换两个数据
var temp = this.array[i]
this.array[i] = this.array[i+1]
this.array[i+1] = temp
}
}
}
}
冒泡排序的比较次数
大O表示法
O(N²)
冒泡排序的交换次数
选择排序思路
思路分析
选择排序的代码分析
代码实现
// 选择排序
ArrayList.prototype.selectionSort = function () {
// 1. 获取数组的长度
var length = this.array.length
// 2. 外层循环:从0位置开始取数据
for (var j = 0; j < length - 1; j++) {
// 内层循环:从 i+1 位置开始,和后面的数据进行比较
var min = j
for (var i = min + 1; i < length; i++) {
if (this.array[min] > this.array[i]) {
min = i
}
}
this.swap(min, j)
}
}
选择排序的比较次数
O(N²)
选择排序的交换次数
插入排序思路
局部有序
插入排序的思路
思路分析
代码解析
代码图解流程
代码实现
// 插入排序
ArrayList.prototype.insertionSort = function () {
// 1. 获取数组的长度
var length = this.array.length
// 2. 外层循环: 从第1个位置开始获取数据,向前面局部有序进行插入
for (var i = 0; i < length; i++) {
// 3. 内层循环: 获取i位置的元素,和前面的数据依次进行比较
var temp = this.array[i]
var j = i
while (this.array[j - 1] > temp && j > 0) {
this.array[j] = this.array[j - 1]
j--
}
// 4. 将j位置的数据,放置temp即可
this.array[j] = temp
}
}
插入排序的比较次数
插入排序的复制次数
对于基本有序的情况
回顾插入排序
插入排序的问题
N*N/2 = N²/2
O(N²)
希尔排序的做法
选择合适的增量
代码解析
代码实现
// 希尔排序
ArrayList.prototype.shellSort = function () {
// 1. 获取数组的长度
var length = this.array.length
// 2. 初始化的增量(gap -> 间隔/间隙)
var gap = Math.floor(length / 2)
// 3. while 循环(gap不断的减小)
while (gap >= 1) {
// 4. 以gap作为间隔,进行分组,对分组进行插入排序
for (var i = gap; i < length; i++) {
var temp = this.array[i]
var j = i
while (this.array[j - gap] > temp && j > gap - 1) {
this.array[j] = this.array[j - gap]
j -= gap
}
// 5. 将j位置的元素赋值temp
this.array[j] = temp
}
// 6. 增量变化 / 2
gap = Math.floor(gap / 2)
}
}
希尔排序的效率
Hibbard 增量序列
Sedgewick增量序列
快速排序的重要性
什么是快速排序
快速排序的思想
分而治之
与冒泡排序的区别
在快速排序中有一个很重要的步骤就是选取枢纽(pivot也人称为主元).
如何选择枢纽
中位数
枢纽代码解析
枢纽选择的代码实现
// 1. 选择枢纽
ArrayList.prototype.median = function (left, right) {
// 1. 取出中间的位置
var center = Math.floor((left + right) / 2)
// 2. 判断大小,并且进行交换
if (this.array[left] > this.array[center]) {
this.swap(left, center)
}
if (this.array[center] > this.array[right]) {
this.swap(center, right)
}
if (this.array[left] > this.array[center]) {
this.swap(left, center)
}
// 3. 将center换到right - 1的位置
this.swap(center, right - 1)
return this.array[right - 1]
}
代码解析
代码实现
// 2. 快速排序的实现
ArrayList.prototype.quickSort = function () {
this.quick(0, this.array.length - 1)
}
ArrayList.prototype.quick = function (left, right) {
// 1. 结束条件
if (left >= right) return
// 2. 获取数据
var pivot = this.median(left, right)
// 3. 定义变量,用于记录当前找到的位置
var i = left
var j = right - 1
// 4. 开始进行交换
while (i < j) {
while (this.array[++i] < pivot) {
}
while (this.array[--j] > pivot) {
}
if (i < j) {
this.swap(i, j)
} else {
break
}
}
// 6. 将枢纽放置在正确的位置,i的位置
this.swap(i, right - 1)
// 7. 分而治之
this.quick(left, i - 1)
this.quick(i + 1, right)
}
最坏情况
平均效率
O(N * logN)
// 快速排序
// 1. 选择枢纽
ArrayList.prototype.median = function (left, right) {
// 1. 取出中间的位置
var center = Math.floor((left + right) / 2)
// 2. 判断大小,并且进行交换
if (this.array[left] > this.array[center]) {
this.swap(left, center)
}
if (this.array[center] > this.array[right]) {
this.swap(center, right)
}
if (this.array[left] > this.array[center]) {
this.swap(left, center)
}
// 3. 将center换到right - 1的位置
this.swap(center, right - 1)
return this.array[right - 1]
}
// 2. 快速排序的实现
ArrayList.prototype.quickSort = function () {
this.quick(0, this.array.length - 1)
}
ArrayList.prototype.quick = function (left, right) {
// 1. 结束条件
if (left >= right) return
// 2. 获取数据
var pivot = this.median(left, right)
// 3. 定义变量,用于记录当前找到的位置
var i = left
var j = right - 1
// 4. 开始进行交换
while (i < j) {
while (this.array[++i] < pivot) {
}
while (this.array[--j] > pivot) {
}
if (i < j) {
this.swap(i, j)
} else {
break
}
}
// 6. 将枢纽放置在正确的位置,i的位置
this.swap(i, right - 1)
// 7. 分而治之
this.quick(left, i - 1)
this.quick(i + 1, right)
}
}