1.排序算法
大O表示法: O(1) < O(log(n))) < O(n) < O(nlog(n)) < O(n2) < O(2n)
1.1 冒泡排序
平均时间复杂度: O(n**2)
最好的情况时间复杂度: O(n)
最差的情况时间复杂度: O(n**2)
空间复杂度: O(1)
排序方式: In-place
稳定性: 稳定
解析:
- 比较相邻的两个元素,如果前一个比后面一个大,则交换位置
- 第一轮结束的时候,最后一个元素应该是最大的
- 第二轮比较的时候,可以剔除最后一个元素了
- 循环
function sortBubble(arr) {
if (!arr || !arr.length || arr.length === 1) {
return arr || []
}
let len = arr.length;
for (let i = 0; i < len - 1; i++) {
let flag = true;
for (let j = 0; j < len - i - 1; j++) {
if (arr[j + 1] < arr[j]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
flag = false; //只要调换,就会置为false
}
}
if (flag) {
break; // 如果遍历一轮,都没交换过,就说明已经提前排序好,提前退出即可
}
}
return arr
}
1.2 选择排序
与冒泡排序比较,交换次数由O(n2)减少到O(n),但 比较次数依旧为n2;
平均时间复杂度: O(n**2)
最好的情况时间复杂度: O(n**2)
最差的情况时间复杂度: O(n**2)
空间复杂度: O(1)
排序方式: In-place
稳定性: 不稳定
function sortSelect(arr) {
if (!arr || !arr.length || arr.length === 1) {
return arr || []
}
for(let i = 0; i < arr.length - 1; i++) {
let min = i;
// 每一轮从当前位置开始,向后找到一个最小的,每一轮能确定一个最小值
for(let j = i+1; j < arr.length; j++) {
if(arr[j] < arr[min]) {
min = j
}
}
if(min !== i){
[arr[i], arr[min]] = [arr[min], arr[i]]
}
}
return arr
}
关于不稳定的解释:
8[1],4,6,8[2],3
排序完成时8[1]和8[2]调换了位置,所以不稳定。
1.3 插入排序
平均时间复杂度: O(n ** 2)
最好的情况时间复杂度: O(n)
最差的情况时间复杂度: O(n ** 2)
空间复杂度: O(1)
排序方式: In-place
稳定性: 稳定
function sortInsert(arr) {
if (!arr || !arr.length || arr.length === 1) {
return arr || []
}
for (let i = 1; i < arr.length; i++) {
for (let j = i - 1; j >= 0; j--) {
if (arr[j] > arr[j + 1]) { // 一位一位往前换
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
} else {
break; //默认前面是已经排序好的,所以可以直接弹出
}
}
}
return arr
}
1.4 希尔排序(插入排序的高效版)
平均时间复杂度: O(n log n)
最好的情况时间复杂度: O(n log^2 n)
最差的情况时间复杂度: O(n log^2 n)
空间复杂度: O(1)
排序方式: In-place
稳定性: 稳定
希尔排序的增量:希尔原稿建议初始间距N/2; Hibbard增量序列 2k -1, 最坏O(n(3/2)); 增量决定了循环起始位置以及插入排序的步长。
function sortShell(arr) {
if (!arr || !arr.length || arr.length === 1) {
return arr || []
}
let gap = Math.floor(arr.length / 2);
while (gap >= 1) {
for (let i = gap; i < arr.length; i++) { // 空出 初始位
let j = i
while (j > gap - 1 && arr[j] < arr[j - gap]) { // 空出 初始位
[arr[j], arr[j - gap]] = [arr[j - gap], arr[j]]
j = j - gap
}
}
gap = Math.floor(gap / 2)
}
return arr
}
1.5 快速排序(冒泡排序的高效版)
平均时间复杂度: O(nlogn)
最好的情况时间复杂度: O(nlogn)
最差的情况时间复杂度: O(n**2)
空间复杂度: O(logn)
排序方式: In-place
稳定性: 不稳定
# 法1 空间换时间 Out-place
function sortQuick(arr) {
if (!arr || !arr.length || arr.length === 1) {
return arr || []
}
let left = [];
let right = [];
let pivot = arr.splice(Math.floor(arr.length / 2), 1)[0]; //随机枢纽
for (let i = 0; i < arr.length; i++) {
if (arr[i] < pivot) {
left.push(arr[i])
} else {
right.push(arr[i])
}
}
return sortQuick(left).concat(pivot, sortQuick(right))
}
# 法2 时间换空间
function swap(arr, l, r) {
[arr[l], arr[r]] = [arr[r], arr[l]]
}
function partition(arr, l, r) {
// 三个指针
let less = l - 1;
let more = r;
// l 为索引
let pivot = arr[r];
while (l < more) { // 当索引l 与 more区左侧相接, 结束遍历
if (arr[l] < pivot) {
swap(arr, ++less, l++) //扩充小区,右移索引
} else if (arr[l] > pivot) {
swap(arr, l, --more)
} else {
l++;
}
}
swap(arr, more--, r);
return [less + 1, more]
}
function sortQuick(arr, left, right) {
if (!arr || !arr.length || arr.length === 1) {
return arr || []
}
if (left < right) {
let pivot = partition(arr, left, right);
sortQuick(arr, left, pivot[0] - 1);
sortQuick(arr, pivot[1] + 1, right);
}
return arr
}
1.6 归并排序
平均时间复杂度: O(nlogn)
最好的情况时间复杂度: O(nlogn)
最差的情况时间复杂度: O(nlogn)
空间复杂度: O(n)
排序方式: Out-place
稳定性: 稳定
function merge(left, right) {
const result = [];
while (left.length && right.length) {
if (left[0] <= right[0]) {
result.push(left.shift())
} else {
result.push(right.shift())
}
}
while (left.length) result.push(left.shift())
while (right.length) result.push(right.shift())
return result
}
function sortMerge(arr) {
if (!arr || !arr.length || arr.length === 1) {
return arr || []
}
let middle = Math.floor(arr.length / 2);
let left = arr.slice(0, middle);
let right = arr.slice(middle);
return merge(sortMerge(left), sortMerge(right))
}
1.7 堆排序
平均时间复杂度: O(nlogn)
最好的情况时间复杂度: O(nlogn)
最差的情况时间复杂度: O(nlogn)
空间复杂度: O(1)
排序方式: In-place
稳定性: 不稳定
function swap(arr, l, r) {
[arr[l], arr[r]] = [arr[r], arr[l]]
}
function heapInsert(arr, index) {
while (arr[index] > arr[(index - 1) >> 1]) {
swap(arr, index, (index - 1) >> 1)
index = (index - 1) >> 1
}
}
function heapify(arr, index, size) {
var left = index * 2 + 1;
while (left < size) { //左孩子没越界
var largest = (left + 1) < size && arr[left] < arr[left + 1] ? left + 1 : left;
largest = arr[index] < arr[largest] ? largest : index;
if (largest == index) {
break;
}
swap(arr, largest, index);
index = largest;
left = index * 2 + 1;
}
}
function sortHeap(arr) {
if (!arr || !arr.length || arr.length === 1) {
return arr || []
}
//建立大顶堆
for (var i = 0; i < arr.length; i++) {
heapInsert(arr, i);
}
var size = arr.length;
swap(arr, 0, --size);
while (size > 0) {
heapify(arr, 0, size); //函数中会0~size-1进行调堆
swap(arr, 0, --size); //再确定一位
}
return arr
}
2.数据结构
数据结构:
就是计算机中 存储和组织数据 的方式; 数据结构的实现离不开算法。
常见的数据结构: 数组/链表/栈/队列/散列表/树/图/堆。
2.1 数组
- 连续的内存空间,
- 线性结构,可在任意位置插入和删除数据;
普通语言中数组封装:
- 常见语言数组不能存放不同的数据类型,同一类型的对象用泛型限制;
- 常见语言数组容量不会自动改变,需进行扩容处理,申请更大数组复制值(创建时申请多少长度的数组);
- 数组中间插入和删除操作性能比较低,(牵一发而动全身);
优点:查找效率高,方便下标取值。
缺点:插入、删除效率比较低。
2.2 栈 (栈是受限的线性结构)
栈顶入栈。LIFO
可基于链表或者数组实现,且性能差异不是很大。
举例:函数调用栈(递归栈溢出错误)
函数之间相互调用A调用B,B调用C,C调用D,
执行过程A先压栈,A没执行完所以不会弹栈;
A执行中调用了B,B先压栈,此时A在栈底,B在栈顶;
若B执行完毕,则B先弹栈A继续执行,若B没执行完并在其中调用C,类似上述操作...
当前栈顺序:栈顶A>>B>>C>>D栈底;
D执行完,弹出栈,C B A依次弹出栈;
// 栈类
function Stack() {
// 栈中的属性
this.items = []
// 栈相关的方法
// 压栈操作
Stack.prototype.push = function (element) {
this.items.push(element)
}
// 出栈操作
Stack.prototype.pop = function () {
return this.items.pop()
}
// peek操作(查看栈顶元素)
Stack.prototype.peek = function () {
return this.items[items.length - 1]
}
// 判断栈中的元素是否为空
Stack.prototype.isEmpty = function () {
return this.items.length == 0
}
// 获取栈中元素的个数
Stack.prototype.size = function () {
return this.items.length
}
}
栈的应用实例:十进制转二进制
function dec2bin(decNumer) {
// 定义变量
var stack = new Stack()
var remainder;
// 循环除法
while (decNumer > 0) {
remainder = decNumer % 2
decNumer = Math.floor(decNumer / 2)
stack.push(remainder)
}
// 将数据取出
var binayriStrng = ""
while (!stack.isEmpty()) {
binayriStrng += stack.pop()
}
return binayriStrng
}
2.3 队列(队列是受限的线性结构)
FIFO
可基于链表或者数组实现,链表实现性能更优。
队列是一种 受限的线性表,只允许在标的前端进行删除,在队列的后端进行插入。这也是用数组实现效率较低的原因。
队列
// 自定义队列类
function Queue(arr) {
this.items = arr || []
// 队列操作的方法
// enter queue方法
Queue.prototype.enqueue = function (element) {
this.items.push(element)
}
// delete queue方法
Queue.prototype.dequeue = function () {
return this.items.shift()
}
// 查看队列前端的元素
Queue.prototype.front = function () {
return this.items[0]
}
// 查看队列是否为空
Queue.prototype.isEmpty = function () {
return this.items.length == 0
}
// 查看队列中元素的个数
Queue.prototype.size = function () {
return this.items.length
}
}
队列应用实例,击鼓传花:
(围成一圈不断地从1数到某个数字,淘汰掉继续,酋最后剩下的人最初的位置。)
// 实现击鼓传花的函数
function passGame(nameList, num) {
// 1.创建一个队列, 并且将所有的人放在队列中
var queue = new Queue(nameList)
// 2.寻找最后剩下的人
while (queue.size() > 1) {
// 将前num-1中的人, 都从队列的前端取出放在 队列的后端
for (var i = 0; i < num - 1; i++) {
queue.enqueue(queue.dequeue())
}
// 将第num个人, 从队列中移除
queue.dequeue()
}
// 3.获取剩下的一个人
var endName = queue.dequeue()
alert("最终留下来的人:" + endName)
// 4.获取该人在队列中的位置
return nameList.indexOf(endName)
}
优先级队列
function PriorityQueue() {
this.items = []
// 封装一个新的构造函数, 用于保存元素和元素的优先级
function QueueElement(element, priority) {
this.element = element
this.priority = priority
}
// 添加元素的方法
this.enqueue = function (element, priority) {
// 1.根据传入的元素, 创建新的QueueElement
var queueElement = new QueueElement(element, priority)
// 2.获取传入元素应该在正确的位置
if (this.isEmpty()) {
this.items.push(queueElement)
} else {
var added = false
for (var i = 0; i < this.items.length; i++) {
// 注意: 我们这里是数字越小, 优先级越高
if (queueElement.priority < items[i].priority) {
this.items.splice(i, 0, queueElement)
added = true
break
}
}
// 遍历完所有的元素, 优先级都大于新插入的元素时, 就插入到最后
if (!added) {
this.items.push(queueElement)
}
}
}
// 删除元素的方法
this.dequeue = function () {
return this.items.shift()
}
// 获取前端的元素
this.front = function () {
return this.items[0]
}
// 查看元素是否为空
this.isEmpty = function () {
return this.items.length == 0
}
// 获取元素的个数
this.size = function () {
return this.items.length
}
}
2.4 链表(线性结构)
优点:
- 不必是连续的空间,链表的每一个元素都是有元素本身的节点和指向下一个元素的引用组成。
- 不必在创建时确定大小。
- 插入和删除操作,时间复杂度可达O(1),效率很高。
缺点:
- 无法通过下标直接访问元素,访问任何位置的一个元素都需要从头开始访问。
2.4.1 单向链表
只能从头遍历到尾或者从尾遍历到头,单向的
function LinkedList() {
# 封装一个Node类, 用于保存每个节点信息
function Node(value) {
this.value = value
this.next = null
}
# 链表中的属性
this.length = 0
this.head = null
#【1】链表尾部追加元素方法
LinkedList.prototype.append = function (value) {
// 1.根据新元素创建节点
var newNode = new Node(value)
// 2.判断原来链表是否为空
if (this.length === 0) { // 链表尾空
this.head = newNode
} else { // 链表不为空
// 2.1.定义变量, 保存当前找到的节点
//找到最后一个节点(next指向null)
var current = this.head;
while (current.next) {
current = current.next
}
// 2.2.找到最后一项, 将其next赋值为node
current.next = newNode
}
// 3.链表长度增加1
this.length++
}
#【2】 链表的toString方法
LinkedList.prototype.toString = function () {
// 1.定义两个变量
var current = this.head
var listString = ""
// 2.循环获取链表中所有的元素
while (current) {
listString += "," + current.value
current = current.next
}
// 3.返回最终结果
return listString.slice(1)
}
#【3】 根据下标添加元素
LinkedList.prototype.insert = function (position, value) {
// 1.检测越界问题: 越界插入失败
//下标从0开始计算;
if (position < 0 || position > this.length) return false
// 2.定义变量, 保存信息
var newNode = new Node(value)
//要插入就要知道前面和当前元素之间插入
var current = this.head
var previous = null
index = 0
// 3.判断是否列表是否在第一个位置插入
if (position == 0) {
newNode.next = current
this.head = newNode
} else {
//!!!!!!!输入index=0的时候重新计算的是index=1的current
while (index++ < position) {
//执行到完一次while时index已经为下次的,所以...
previous = current //前面的那个元素
current = current.next
}
newNode.next = current
previous.next = newNode
}
// 4.length+1
this.length++
return true
}
#【4】get方法
LinkedList.prototype.get = function(position){
// 1.检测越界问题: 越界移除失败, 返回null
if (position < 0 || position >= this.length) return null
//2.获取对应的数据
var current = this.header
var index = 0;
while(index++ < position){
current = current.next;
}
return current.value;
}
#【5】根据元素获取链表中第一个的位置indexOf
LinkedList.prototype.indexOf = function (value) {
// 1.定义变量, 保存信息
var current = this.head
index = 0
// 2.找到第一个元素所在的位置
while (current) {
if (current.value === value) {
return index
}
index++
current = current.next
}
// 3.来到这个位置, 说明没有找到, 则返回-1
return -1
}
#【6】update修正该指定位置的信息
LinkedList.prototype.update = function(position,value){
// 1.检测越界问题: 越界移除失败, 返回null
if (position < 0 || position >= this.length) return null
var current = this.header;
var index = 0;
while(index++ < position ){
current = current.next;
}
current.value = value;
return true;
}
#【7】根据位置删除节点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.value
}
#【8】 根据元素删除信息
LinkedList.prototype.remove = function (value) {
var index = this.indexOf(value)
return this.removeAt(index)
}
#【9】判断链表是否为空
LinkedList.prototype.isEmpty = function () {
return this.length == 0
}
#【10】获取链表的长度
LinkedList.prototype.size = function () {
return this.length
}
#【11】 获取第一个节点
LinkedList.prototype.getFirst = function () {
return this.head.value
}
}
2.4.2 双向链表
既可以从头遍历到尾又可以从尾遍历到头
除了header指向第一个节点外,还有一个tail指向最后一个节点。
# 创建双向链表的构造函数
function DoublyLinkedList() {
# 创建节点构造函数
function Node(value) {
this.value = value
this.next = null
this.prev = null // 新添加的
}
# 定义属性
this.length = 0
this.head = null
this.tail = null // 新添加的
# 定义相关操作方法
#【1】在尾部追加数据append
DoublyLinkedList.prototype.append = function(value){
// 1.根据元素创建节点
var newNode = new Node(value);
// 2.判断列表是否为空
if(this.length === 0){
this.head = newNode;
this.tail = newNode;
}else{
this.tail.next = newNode;
newNode.prev = this.tail;
this.tail = newNode;
}
//3.length++
this.length++;
}
#【2】将链表转成字符串的形式
//2.1 正向遍历 forwardString
DoublyLinkedList.prototype.forwardString = function(){
var current = this.head;
var forwardStr = ""
while(current){
forwardStr += ","+current.value;
current = current.next;
}
return forwardStr.slice(1);
}
//2.2 反向遍历 reverseString
DoublyLinkedList.prototype.reverseString = function(){
var current = this.tail;
var reverseStr = ""
while(current){
reverseStr += ","+current.value;
current = current.prev;
}
return reverseStr.slice(1);
}
#【3】在任意位置插入数据
DoublyLinkedList.prototype.insert = function (position, value) {
// 1.判断越界的问题
if (position < 0 || position > this.length) return false
// 2.创建新的节点
var newNode = new Node(value)
// 3.判断插入的位置
if (position === 0) { // 在第一个位置插入数据
// 判断链表是否为空
if (this.length === 0) {
this.head = newNode
this.tail = newNode
} else {
this.head.prev = newNode
newNode.next = this.head
this.head = newNode
}
} else if (position === this.length) { // 插入到最后的情况
this.tail.next = newNode
newNode.prev = this.tail
this.tail = newNode
} else { // 在中间位置插入数据
// 定义属性
var index = 0
var current = this.head
var previous = null
// 查找正确的位置
while (index++ < position) {
previous = current
current = current.next
}
// 交换节点的指向顺序
newNode.next = current
newNode.prev = previous
current.prev = newNode
previous.next = newNode
}
// 4.length+1
this.length++
return true
}
#【4】get方法获取指定位置元素的值
DoublyLinkedList.prototype.get= function(position){
// 1.判断越界的问题
if (position < 0 || position >= this.length) return null;
if(position<Math.floor(this.length/2)){
var current = this.head;
var index = 0;
while (index++ < position){
current=current.next;
}
}else{
var current = this.tail;
var index = 0;
while(index++ < this.length - position){
current=current.prev
}
}
return current.element;
}
//注意 insert以及get,removeAt这些都可以优化
通过position与length/2进行比较,确定从头到尾找还是从尾到头找更快
#【5】indexOf返回元素的索引
DoublyLinkedList.prototype.indexOf = function(value){
var current = this.head;
var index = 0;
while(current){
if(current.value === value){
return index
}
index += 1;
current = current.next;
}
return -1;
}
#【6】update修改指定位置的元素内容
DoublyLinkedList.prototype.update = function(position,value){
//1.处理越界
if(position<0 ||position >= this.length){
return false;
}
//2.
var currrent = this.head;
var index = 0;
while(index++ < position){
current = current.next;
}
current.value = value;
return true;
}
#【7】removeAt 删除指定位置的元素
DoublyLinkedList.prototype.removeAt = function(position){
// 1.判断越界的问题
if (position < 0 || position >= this.length) return null;
// 2.判断移除的位置
if(position === 0){
if(this.length === 1){
this.head = null;
this.tail = null;
}else{
this.head = this.head.next;
this.head.prev = null;
}
}else if(position === this.length-1){
this.tail = this.tail.prev;
this.tail.next = null;
}else{
var index = 0;
var current = this.head;
var previous = null;
while(index++ <position){
previous = current;
current = current.next;
}
previous.next = current.next;
current.next.prev = previous;
}
this.length -- ;
return current.value;
}
#【8】remove
DoublyLinkedList.prototype.remove = function(value){
var index = this.indexOf(value);
return this.removeAt(index)
}
#【9】判断是否为空isEmpty
DoublyLinkedList.prototype.isEmpty = function () {
return this.length === 0
}
#【10】 获取链表长度size
DoublyLinkedList.prototype.size = function () {
return this.length
}
#【11】获取链表的第一个元素或者最后一个元素
// 获取第一个元素
DoublyLinkedList.prototype.getHead = function () {
return this.head.value
}
// 获取最后一个元素
DoublyLinkedList.prototype.getTail = function () {
return this.tail.value
}
}
2.5 集合
比较常见的实现方式是哈希表。
集合特点:无序、不能重复。可看做 特殊的数组。
# 封装集合的构造函数
function Set() {
//!!!!!!!!!! 使用一个对象来保存集合的元素,格式{key:key}
this.items = {}
}
# 集合的操作方法
// 判断集合中是否有某个元素
Set.prototype.has = function (value) {
return this.items.hasOwnProperty(value)
}
// 向集合中添加元素
Set.prototype.add = function (value) {
// 1.判断集合中是否已经包含了该元素
if (this.has(value)) return false
// 2.将元素添加到集合中
this.items[value] = value
return true
}
// 从集合中删除某个元素
Set.prototype.remove = function (value) {
// 1.判断集合中是否包含该元素
if (!this.has(value)) return false
// 2.包含该元素, 那么将元素删除
delete this.items[value]
return true
}
// 清空集合中所有的元素
Set.prototype.clear = function () {
this.items = {}
}
// 获取集合的大小
Set.prototype.size = function () {
return Object.keys(this.items).length
}
// 获取集合中所有的值
Set.prototype.values = function () {
return Object.keys(this.items)
}
# 集合间的操作
// 1 集合并集操作, 返回新集合
Set.prototype.union = function (otherSet) {
//this 当前集合A
//otherSet 集合B
// 返回创建新的集合;
var unionSet = new Set();
//将当前集合的所有元素添加到新集合中;
var values = this.values();
for (var i = 0; i < values.length; i++) {
unionSet.add(values[i]);
}
//取出B集合中的所有元素存储到unionSet中;
values = otherSet.values();
for (var i = 0; i < values.length; i++) {
unionSet.add(values[i]);
}
return unionSet;
}
// 2 集合交集操作, 返回新集合
Set.prototype.intersection = function (otherSet) {
var intersection = new Set();
var values = this.values();
for (var i = 0; i < values.length; i++) {
var item = values[i];
if (otherSet.has(item)) {
intersection.add(item)
}
}
return intersection;
}
// 3 集合差集操作, 返回新集合
Set.prototype.defference = function (otherSet) {
var defference = new Set();
var values = this.values();
for (var i = 0; i < values.length; i++) {
var item = values[i];
if (!otherSet.has(item)) {
defference.add(item)
}
}
return defference;
}
// 4 是否是集合子集操作
// 集合A(this当前集合) 是否是集合B(otherSet) 的子集
Set.prototype.subSet = function (otherSet) {
var isSubSet = true;
var values = this.values();
for (var i = 0; i < values.length; i++) {
var item = values[i];
if (!otherSet.has(item)) {
isSubSet = false;
}
}
retrun isSubSet;
}
2.6 字典
可基于对象或者哈希表实现
一一对应的键值对,key是不可以重复的;key是无序的
# 创建字典的构造函数
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 = {}
}
2.7 哈希表
背景:
数组插入和删除操作的效率比较低;
数组进行查找操作的效率:
1)基于索引查找效率高;
2)基于内容查找效率低(得遍历);
哈希表是基于数组实现的,
但是相对于数组存在优势:
1)插入、删除、查找效率非常高;
2)插入删除时间复杂度接近O(1);
3)哈希表的速度比树要快;
缺陷:
1)数据无序的;
2)key不能重复;
3)空间利用率不高;
哈希函数:
将要查询的内容 对应到 数组中的下标值。
比如 name(Lucy) ====> 下标 (0002)
将单词转成大数字,大数字再进行哈希化。
(1)字母转成大数字(幂的连乘)
数字相加的方案过于简单了,cats为例字母转数字,一种对应下标的理想的方案是 幂的连乘。
cats 对应到 3*(27**3)+1*(27**2)+20*(27**1)+17=60337
保证了单词的唯一性,但数字比较大 =====> 进行哈希化改进,类似对应到ascii码表中
(2)哈希化:
将大数字转化(压缩)成数组范围内下标的过程(模值取余)。
哈希表:
数据整体存储在数组中。
按照哈希函数对应的下标在数组中存储对应的信息。
冲突:
哈希化的下标值依然可能重复。
举例:
0-199的数字选取5个放在长度为10的单元格中,
如果我们随机选出来的是33/82/11/45/90,
那么最终他们的位置会是3,2,1,5,0,(除以将要放到的数组的长度)没有发生冲突;
如果有一个33,还有一份73,就会冲突。
解决冲突:
1)链地址法:
数组中每个位置放 链表或数组(叫做链条),在其中存放对应元素。按照业务需求,新数据使用频率高,插入到链表前面。
2)开放地址法:
寻找空白的单元格来添加重复(冲突)的数据。
寻找空白单元格(探测):
1) 线性探测:
线性的查找空白单元格,
插入:
若哈希化的下标位置已有元素,则从index+1位置开始查找合适(空白)的位置插入当前元素。
查找:
要查找的元素32哈希化后,到数组的指定下标位置2处去查找对比数据82,若不同,则index+1继续查找对比。
查询过程有一个约定,查询到空位置就停止。
删除:
将已插入的元素删除,给其赋值-1,否则若设为null,比如数组中存有32,42,62如果把42删除设为null,之后查找时便不会往下继续,找不到62。
缺点:线性探测比较严重的问题就是聚集性能,聚集指的是一连串填充单元。需要探测很长的距离。
2) 二次探测:
在探测的基础上进行优化,
优化的是探测的步长。
比如x+1**2,x+2**2,x+3**2,解决聚集性问题。
3) 再哈希:
消除线性探测和二次探测中无论步长+1还是步长+平方中存在的问题。
把关键字(初始下标,如上述方法32,82的2)用另外一个哈希函数,再做一次哈希化,本次哈希的结果作为步长。
对于指定的关键字,步长在整个探测中是不变的,不同的关键字使用不同的步长。
要求:与第一个哈希函数不同,且步长不能为0。
setpSize = constant - (key % constant);constant为质数。
哈希化效率
哈希化效率:
哈希表中如果发生冲突,存取时间就依赖后来的探测长度。
平均探测长度以及平均存取时间取决于填装因子,随着填装因子越来越大,探测长度也越来越大,效率下降。
填装因子:
表示当前哈希表中已经包含的数据项和整个哈希表长度的比值。
填装因子 = 总数据项 / 哈希表长度。
通常,填装因子大于0.75会对数组进行扩容,小于0.25时缩小数组大小。
开放地址法的最大填装因子为1;
链地址法的最大填装因子可以大于1;
所以实际开发中链地址法用到的频率会更高,性能比较平均。
优秀的哈希函数
优秀的哈希函数:
哈希函数中尽可能少乘法和除法。计算机进行乘除性能比较差。
优秀的哈希函数应具备:快速计算和均匀分布。
- 快速计算:
任何一个单词转成数字都是一个多项式的计算过程。
a(n)*(x**n)+a(n-1)*(x**(n-1)))+...+a(1)*x+a(0)
乘法次数:n+(n-1)+...+1=n(n+1)/2
加法次数:n次
霍纳法则:(多项式优化)
((...(((a(n)*x + a(n-1))*x + a(n-2))*x+a(n-3))...)*x +a(1))*x+a(0)
优化后乘法N次,加法N次。
- 均匀分布:
使用常量的地方尽量的使用质数。
eg:哈希表的长度,N次幂的底数;
链地址法中质数没那么重要。
代码实例
哈希表封装:
//创建HashTable构造函数
//使用 链地址法实现 哈希表
//数据结构[ [[key,val],[key,val],[key,val]], [[k,v],[k,v],[k,v]] ]
function HashTable() {
// 定义属性
this.storage = [];
this.count = 0;
// 记录当前哈希表中已存多少元素,用于计算填装因子,
// 当填装因子大于0.75会进行扩容,小于0.25时缩小数组。
this.limit = 8;// 数组范围,哈希函数size
}
# 哈希函数封装:
1> 字符串 转成 比较大的数字 hashCode;
2> 将比较大的数字 压缩到 数组范围(this.limit)内;
HashTable.prototype.hashFunc = function (str, size) {
// 1.初始化hashCode的值
var hashCode = 0
// 2.霍纳算法, 来计算hashCode的数值
for (var i = 0; i < str.length; i++) {
hashCode = 37 * hashCode + str.charCodeAt(i)
}
// 3.取模运算
var index = hashCode % size
return index
}
# HashTable常用的方法集合
#【2】插入&修改数据方法 [[k, v], [k, v]]
HashTable.prototype.put = function (key,value) {
// 1.获取key对应的index
var index = this.hashFunc(key, this.limit)
// 2.取出数组(也可以使用链表)
var bucket = this.storage[index]
// 3.判断这个数组是否存在
if (bucket === undefined) {
// 3.1创建桶
bucket = []
this.storage[index] = bucket
}
// 4.判断是新增还是修改原来的值.
var override = false
for (var i = 0; i < bucket.length; i+ +) {
var tuple = bucket[i]
if (tuple[0] === key) {
tuple[1] = value
override = true
}
}
// 5.如果是新增, 前一步没有覆盖
if (!override) {
bucket.push([key, value])
this.count++;
if (this.count > this.limit * 0.75) {
var primeNum = this.getPrime(this. limit * 2);
this.resize(primeNum);
}
}
};
#【3】获取存放的数据
HashTable.prototype.get = function (key){
// 1.获取key对应的index
var index = this.hashFunc(key, this.limit)
// 2.获取对应的bucket
var bucket = this.storage[index]
// 3.如果bucket为null, 那么说明这个位置没有数据
if (bucket == null) {
return null
}
// 4.有bucket, 判断是否有对应的key
for (var i = 0; i < bucket.length; i++) {
var tuple = bucket[i]
if (tuple[0] === key) {
return tuple[1]
}
}
// 5.没有找到, return null
return null
}
#【4】 删除数据
HashTable.prototype.remove = function(key) {
// 1.获取key对应的index
var index = this.hashFunc(key, this.limit)
// 2.获取对应的bucket
var bucket = this.storage[index]
// 3.判断同是否为null, 为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--;
// 缩小数组的容量
if (this.limit > 7 && this.count < this.limit * 0.25) {
var primeNum = this.getPrime(Math.floor(this.limit / 2));
this.resize( primeNum)
}
}
return tuple[1]
}
// 5.来到该位置, 说明没有对应的数据, 那么返回null
return null
}
#【5】 isEmpty方法
HashTable.prototype.isEmpty = function () {
return this.count == 0
}
#【6】 size方法
HashTable.prototype.size = function () {
return this.count
}
#【7】哈希表扩容
# 为什么?
# 因为 随着数据量的增多,填装因子会越来越大,效率就会越来越低。
# 注意:扩容时全部数据要重新计算下标,重新插入。
HashTable.prototype.resize = function(newLimit) {
// 1.保存旧的数组内容
var oldStorage = this.storage
// 2.重置属性
this.limit = newLimit
this.count = 0
this.storage = []
// 3.遍历旧数组中的所有数据项, 并且重新插入到哈希表中
oldStorage.forEach(function (bucket){
// 1.bucket为null, 说明这里面没有数据
if (bucket == null) {
continue
}
// 2.bucket中有数据, 那么将里面的数据重新哈希化插入
for (var i = 0; i < bucket.length; i++) {
var tuple = bucket[i]
this.put(tuple[0], tuple[1])
}
}).bind(this)
}
#【8】扩容成 limit为质数的数组(数组长度为质数)
# 判断n是否为质数时,不需要每次都从2到n-1都进行判断。
# 更高效的指数判断:
# 因为一个数若可以进行因数分解,那么分解得到的两个数一定是一个小于等于sqrt(n),一个大于等于sqrt(n).
// 判断是否是质数
HashTable.prototype.isPrime = function (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 (!isPrime(num)) {
num++
}
return num
}
2.8 树(非线性结构)
树的特性:
节点的度(Degree):节点的子树个数(当前节点有几个子节点);
叶子节点:度为0的节点;
树的度:树的所有节点的最大的度数;
路径及路径长度:包含边的个数,而不是节点的个数。
节点的层次:规定根节点在1层;
树的深度:所有节点中的最大层次;
儿子-兄弟表示法:
对于每个节点度不定的树,建议使用儿子-兄弟表示法。
记录每个点左边第一个孩子 以及 右边的第一个兄弟;leftChild,rightSibling
儿子兄弟表示法旋转:
形成一棵二叉树。
因此,所有的树本质上都可以使用二叉树模拟出来。
2.8.1 二叉树
树的度为2;
第i层的最大节点数为2**(i-1);
深度为k的二叉树的最大节点数2**k - 1;
叶子节点的个数 = 度为2的节点的个数+1;
节点n的左子节点为2n,右子节点为2n+1;
完美二叉树perfect binary tree,满二叉树:
叶节点都在树的最下一层,且除了该层外的每层节点都有两个子节点。
完全二叉树complete binary tree:
除了二叉树的最后一层外,其他各层节点数为当前层的最大个数,
且最后一层从左到右的叶节点联系存在,只能缺右侧的若干个节点。
二叉树的存储:
常见的存储方式是链表和数组;
完全二叉树:数组;
非完全二叉树:数组会造成空间浪费;
2.8.2 二叉搜索树 BST
# 创建BinarySearchTree
function BinarySerachTree() {
// 创建节点构造函数
function Node(val, left, right) {
this.val = val;
this.left = left || null;
this.right = right || null;
}
// 保存根的属性
this.root = null;
}
# 二叉搜索树相关的操作方法
#【 1】 向树中插入数据
BinarySearchTree.prototype.insert = function (val) {
// 1.根据key创建对应的node
var newNode = new Node(key);
// 2.判断根节点是否有值
if (!this.root) {
this.root = newNode;
} else {
//while或者递归实现;
this.insertNode(this.root, newNode);
//while逻辑相同只是通过控制current来进行查找
}
}
BinarySerachTree.prototype.insertNode = function (node, newNode) {
if (newNode.val < node.val) { // 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)
}
}
}
#【 2】 定义二叉树查询方法
BinarySerachTree.prototype.find = function (data) {
//【2.1】递归实现
return this.searchNode(this.root, data);
//【2.2】while循环实现
let curNode = this.root;
while (true) {
if (curNode.val === data) {
return curNode;
}
curNode = curNode.val > data ? curNode.left : curNode.right;
if (!curNode) { //找不到的情况
return false;
}
}
}
BinarySearchTree.prototype.searchNode = function (node, val) {
if (node === null) {
return false
}
if (node.val > val) { // 2.1.传入的key较小, 向左边继续查找
return this.searchNode(node.left, val)
} else if (node.val < val) { // 2.2.传入的key较大, 向右边继续查找
return this.searchNode(node.right, val)
} else { // 2.3.相同, 说明找到了key
return node
}
}
#【 3】 二叉搜索树的遍历
# 3.1 先序遍历
# 3.1 .1 递归实现
BinarySearchTree.prototype.preOrderTraversal = function (handler) {
this.preOrderTraversalNode(this.root, handler)
}
BinarySerachTree.prototype.preOrderTranversalNode = function (node, handler) {
if (node !== null) {
handler(node.val);
this.preOrderTranversalNode(node.left, handler);
this.preOrderTranversalNode(node.right, handler);
}
}
# 3.1 .2 非递归实现
BinarySearchTree.prototype.preOrderTraversal = function (handler) {
const root = this.root;
if (root) {
const stack = [root];
while (stack.length) {
const node = stack.pop();
// console.log(node.val);
handler(node.val)
if (node.right) { //注意,先入右,再入左,出栈就是中左右的顺序了
stack.push(node.right)
} else if (node.left) {
stack.push(node.left)
}
}
}
}
# 3.2 中序遍历
# 3.2 .1 递归实现
BinarySerachTree.prototype.midOrderTraversal = function (handler) {
this.midOrderTraversalNode(this.root, handler)
}
BinarySerachTree.prototype.midOrderTraversalNode = function (node, handler) {
if (node !== null) {
this.midOrderTraversalNode(node.left, handler);
handler(node.val);
this.midOrderTraversalNode(node.right, handler);
}
}
# 3.2 .2 非递归实现
BinarySerachTree.prototype.midOrderTraversal = function (handler) {
const root = this.root;
if (root) {
let stack = [root];
while (stack.length) {
if (root.left) {
stack.push(root.left) // 左侧全部压栈
root = root.left;
} else {
//左侧栈顶元素没有再左的了,弹出栈顶元素
const node = stack.pop();
// console.log(node.val)
handler(node.val)
// 如果当前栈顶元素有右侧分支
if (node.right) {
stack.push(node.right);
root = node.right; // 右侧分支的左分支全部压栈
}
// 没有的话会继续弹出栈顶节点
}
}
}
}
# 3.3 后序遍历
# 3.3 .1 递归实现
BinarySerachTree.prototype.postOrderTraversal = function (handler) {
this.postOrderTraversalNode(this.root, handler)
}
BinarySerachTree.prototype.postOrderTraversalNode = function (node, handler) {
if (node !== null) {
this.postOrderTraversalNode(node.left, handler)
this.postOrderTraversalNode(node.right, handler)
handler(node.val)
}
}
# 3.3 .2 非递归实现
BinarySerachTree.prototype.postOrderTraversal = function (handler) {
if (root) {
let stack1 = [root];
let stack2 = [];
// 后序遍历是先左再右最后根
// 所以对于一个栈来说,应该先 push 根节点
// 然后 push 右节点,最后 push 左节点
while (stack1.length) {
// stack01
// right
// left
// mid
let node = stack1.pop();
stack2.push(node.val);
if (node.left) { // 先放左再方右
stack1.push(node.left)
}
if (node.right) {
stack1.push(node.right)
}
}
// stack02
// left
// right
// mid
while (stack2.length > 0) {
handler(stack2.pop());
}
}
}
# 3.4 层序遍历
BinarySerachTree.prototype.levelOrderTraversal = function (handler) {
const root = this.root;
let res = [];
if (!root) {
return res;
}
const queue = [root]; //初始队列
while (queue.length) {
let len = queue.length; //当前层节点的数量
const tempArr = []; //新的层数组
for (let i = 0; i < len; i++) {
const node = queue.shift();
tempArr.push(node.val);
if (node.left) q.push(node.left); //检查左节点,存在左节点就继续加入队列
if (node.right) q.push(node.right); //检查左右节点,存在右节点就继续加入队列
}
res.push(tempArr) //推入当前层的数组
}
return res;
}
#【 4】 最大值, 最小值
BinarySearchTree.prototype.min = function () {
var current = this.root;
var val = null;
while (current !== null) {
val = current.val;
current = current.left;
};
return val;
}
BinarySearchTree.prototype.max = function () {
var current = this.root;
var val = null;
while (current !== null) {
val = current.val;
current = current.right;
};
return val;
}
#【 5】 删除
BinarySearchTree.prototype.remove = function (data) {
let cur = this.root;
let delNode = null;
let delParent = null;
let isLeft = true; // delNode 在 delParent 的左支还是右支;
// 找出删除节点及其父节点
while (cur && cur.val !== data) {
delParent = cur;
if (cur.val > data) {
isLeft = true;
cur = cur.left;
} else {
isLeft = false;
cur = cur.right;
}
}
if (cur.val !== data) {
return false; //没找到当前值的节点,删除失败
}
let isRoot = this.root == cur;
delNode = cur;
// 缺少找不到的情况处理方案
// 1)删除节点没有子节点的情况
if (!delNode.left && !delNode.right) {
if (isRoot) {
this.root = null
} else {
if (isLeft) {
delParent.left = null
} else {
delParent.right = null
}
}
}
// 2)删除节点只有一个子节点
if (delNode.left ^ delNode.right) {
if (isLeft) {
delParent.left = delNode.left || delNode.right || null;
} else {
delParent.right = delNode.left || delNode.right || null;
}
}
// 3)删除节点有两个子节点
if (delNode.left && delNode.right) {
// 找其后继节点
let backsuccussor = delNode.right;
let backsuccussorParent = delNode.right;
while (backsuccussor.left) {
backsuccussorParent = backsuccussor;
backsuccussor = backsuccussor.left
};
//后继节点,其父一定有left,本身一定无左支。
backsuccussorParent.left = backsuccussor.right;
backsuccussor.left = delNode.left;
backsuccussor.right = delNode.right;
if (isRoot) {
this.root = backsuccussor;
} else if (isLeft) {
delParent.left = backsuccussor;
} else {
delParent.right = backsuccussor
}
}
return true;
}
#【6】翻转二叉树
var invertTree = function(root) {
if(!root) return root;
invertTree(root.right);
invertTree(root.left);
[root.left,root.right] = [root.right,root.left];
return root;
};
二叉搜索树的缺陷:
- 二叉树的效率与树的层次有关;
- 如果连续插入一组有序的数据,树的一枝就会不断地加深,大大降低树的效率;
非平衡树:
- 比较好的二叉搜索树的数据应该是左右均匀分布的。
- 对于一棵平衡树来说,插入/查找效率应该是O(logN),
- 对于一棵非平衡树,相当于编写一个链表,插入/查找效率是O(N);
所以,为了较快时间来操作一棵树,我们要尽量保证树是平衡的。
常见的平衡树有:
- AVL树和红黑树;
- AVL树的整体效率不如红黑树;