一、数据结构和算法
1.1 数据结构
数据结构是计算机中存储、组织数据的方式。比如图书馆里存放书籍,当需要查找某一本书籍时能以合理的方式迅速找到
1.2 算法(Olgorithm)
解决问题的办法/步骤逻辑。数据结构的实现离不开算法
生活中的算法:
拿快递:当报出名字跟号码时,快递小哥能迅速找到你的快递
方式:
-
线性查找
一个一个比对,最终找到
-
二分查找
从中间位置开始查找,先看一头有没有问题,如果没有则从另一头取中间点再次查找,以此类推
二、线性和非线性数据结构
线性结构:数据元素之间一对一的关系
非线性结构:数据元素之间一对多的关系
存储方式
顺序存储:Set、Array
散列存储:Map、Object
链式存储:链表
2.1 线性结构
2.1.1 数组
数组是一种线性结构,可以在数组的任意位置插入和删除数据
- JS 的数组就是API的调用
- 普通语言的数组封装(比如Java的ArrayList)
- 不能存放不同的数据类型,通常在封装时存放在数组中的是 Object 类型
- 容量不会自动改变(需要进行扩容处理)
- 进行中间插入和删除操作性能比较低
- 根据下标查找值效率较高,根据内容查找值效率较低
2.1.2 栈(stack)
栈是一种受限的线性表,先进后出(LIFO last in first out)
概念
- 仅允许在表的一端进行插入和删除运算
- LIFO(last in first out)表示后进入的元素,先弹出栈空间
- 向栈插入新元素又称作进栈、入栈或压栈,把新元素放到栈顶元素的上面,使之成为新的栈顶元素
- 向栈删除元素又称作出栈或退栈,先把栈顶元素删掉,其相邻的元素成为新的栈顶元素
生活中的栈:自助餐的托盘
应用
- A函数 调用 B函数,B函数又 调用 C函数
- 递归,不断调用自己
实现
function Stack() {
this.items = []
// 1. 添加
Stack.prototype.push = function (element) {
this.items.push(element)
}
// 2. 取元素
Stack.prototype.pop = function () {
return this.items.pop()
}
// 3. 查看栈顶元素
Stack.prototype.peek = function () {
return this.items[-1]
}
// 4. 判断栈是否为空
Stack.prototype.isEmpty = function () {
return this.items.length === 0
}
// 5. 获取栈中的元素个数
Stack.prototype.size = function () {
return this.items.length
}
// 6. toString
Stack.prototype.toString = function () {
let resultString = ''
for (const i = 0;i < this.items.length;i++)
resultString += this.items[i] + ' '
}
return resultString
}
const s = new Stack()
操作
- push(element):添加一个新元素到栈顶位置
- pop():移除栈顶的元素,同时返回被删除的元素
- peek():返回栈顶的元素,不对栈做任何修改
- isEmpty():如果栈里没有任何元素返回 true,否则返回 false
- size():返回栈里的元素个数
- toString():将栈结构的内容以字符形式返回
十进制转二进制:
function dec2bin(decNumber) {
// 1. 定义栈对象
const stack = new Stack()
// 2. 循环操作
while (decNumber > 0) {
// 2.1 获取余数,放入栈中
stack.push(decNumber % 2)
// 2.2 获取整除后的结果,作为下一次运行的数字
decNumber = Math.floor(decNumber / 2)
}
// 3. 从栈中取出 0 和 1
let binaryString = ''
while (!stack.isEmpty()) {
binaryString += stack.pop()
}
return binaryString
}
console.log(dec2bin(10)) // 1010
2.1.3 队列(Queue)
队列是一种受限的线性表,先进先出(FIFO first in first out)
特点
- 只允许在表的前端(front)进行删除操作
- 而在表的后端(rear)进行插入操作
应用
- 打印队列
- 有五份文档需要打印,按次序放入打印队列中
- 打印机会依次从队列中取出文档,先放入的文档优先取出
- 以此类推,直到队列中不再有新的文档
- 线程队列
- 在开发中,为了让任务可以并行处理,通常会开启多个线程
- 但是不能让大量的线程同时运行处理任务(占用过多的资源)
- 这个时候可以使用线程队列,按次序启动线程,并且处理对应的任务
实现
function Queue() {
this.items = []
// 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 () {
let resultString = ''
for (const i = 0;i < this.items.length;i++)
resultString += this.items[i] + ' '
}
return resultString
}
const s = new Queue()
操作
- enqueue(element):添加一个新元素到队列后端
- dequeue():移除队列的前端元素,同时返回被删除的元素
- front():返回队列的最前端元素,不对栈做任何修改
- isEmpty():如果队列里没有任何元素返回 true,否则返回 false
- size():返回队列里的元素个数
- toString():将队列结构的内容以字符形式返回
优先级队列
在插入一个元素的时候会考虑该元素的优先级,和其他元素优先级进行比较,比较完成后得出这个元素在队列中正确的位置
例如:登记顺序,头等舱和商务舱的乘客优先级要高于经济舱的乘客
2.1.4 链表
概念
要存储多个元素,另外一个选择就是链表。不同于数组,链表中的元素在内存中不必是连续的空间。链表的每一个元素由一个存储**元素本身的节点(item)和一个指向下一个元素的引用(next)**组成
比如:火车,一个火车头,连接一个个车厢(数据)
相比数组的优缺点
优点
- 内存空间不是必须连续的,可以充分利用计算机的内存实现灵活的内存动态管理
- 链表不必在创建时就确定大小, 可以无限的延伸下去
- 链表在插入和删除数据时,时间复杂度可以达到O(1),相对数组效率高很多
缺点
- 链表无论访问任何一个位置的元素,都要从头开始访问
- 无法通过下标直接访问元素,需要从头一个个访问,直到找到对应的元素
实现
function NodeList() {
function Node(value) {
this.value = value
this.next = null
}
this.size = 0 // 链表长度
this.headNode = new Node(null) // 链表头部
// 尾部添加一个新的项
NodeList.prototype.append = function(data) {
// 1.创建新的节点
const newNode = new Node(data)
// 2.判断是否添加的是第一个节点
if (this.size === 0) { // 2.1 是第一个节点
this.headNode = newNode
} else { // 2.2 不是第一个节点
let current = this.headNode
// 找到最后一个节点
while (current.next) {
current = current.next
}
current.next = newNode
}
// 3.size ++
this.size++
}
// toString
NodeList.prototype.toString = function() {
let current = this.headNode
let listString = ''
while (current) {
listString += current.value + ' '
current = current.next
}
return listString
}
// 插入
NodeList.prototype.insert = function(position, data) {
// 1.对 position 进行越界判断
if (position < 0 || position > this.size) throw new Error('position 越界了')
// 2.创建新的节点
const newNode = new Node(data)
// 3.判断插入位置
if (position === 0) { // 3.1 第一个节点
newNode.next = this.headNode
this.headNode = newNode
} else { // 3.2 不是第一个节点
let index = 0
let current = this.headNode
let prev = null
while (index++ < position) {
prev = current
current = current.next
}
prev.next = newNode
newNode.next = current
}
// 4.size ++
this.size++
}
// 获取
NodeList.prototype.get = function(position) {
// 1.对 position 进行越界判断
if (position < 0 || position >= this.size) throw new Error('position 越界了')
// 2.查找正确的节点
let index = 0
let current = this.headNode
while (index++ < position) {
current = current.next
}
// 3.返回对应的值
return current.value
}
// 更新
NodeList.prototype.update = function(position, newData) {
// 1.对 position 进行越界判断
if (position < 0 || position >= this.size) throw new Error('position 越界了')
// 2.查找正确的节点
let index = 0
let current = this.headNode
while (index++ < position) {
current = current.next
}
// 3.更新对应的值
current.value = newData
}
// 删除
NodeList.prototype.remove = function(position) {
// 1.对 position 进行越界判断
if (position < 0 || position > this.size) throw new Error('position 越界了')
// 2.判断插入位置
if (position === 0) { // 2.1 第一个节点
this.headNode = this.headNode.next
} else { // 2.2 不是第一个节点
let index = 0
let current = this.headNode
let prev = null
while (index++ < position) {
prev = current
current = current.next
}
prev.next = current.next
}
// 3.size --
this.size--
}
NodeList.prototype.size = function() {
return this.size
}
NodeList.prototype.isEmpty = function() {
return this.size === 0
}
}
常见的操作
- append(data):向链表尾部添加一个新的项
- insert(position, data):向链表的特定位置插入一个新的项
- get(position):获取对应位置的元素
- update(position, newData):修改某个位置的元素
- remove(position):从列表的定位置移除一项
- isEmpty():如果链表中不包含任何元素,返回 true,反之 false
- size():返回链表中包含的元素个数
- toString():将链表结构的value内容以字符形式返回
2.2 非线性结构
2.2.1 集合
概念
- 通常由一组无序的、不能重复的元素构成
- 不能通过下标值进行访问,相同的对象在数组中只存在一份
实现
function Set() {
this.items = {}
// add 方法
Set.prototype.add = function (value) {
// 判断集合中是否已经包含了元素
if (this.has(value)) {
return false
}
this.items[value] = value
return true
}
// has 方法
Set.prototype.has = function (value) {
return this.items.hasOwnProperty(value)
}
// remove 方法
Set.prototype.remove = function (value) {
// 判断集合中是否已经包含了元素
if (!this.has(value)) {
return false
}
delete this.items[value]
return true
}
// clear 方法
Set.prototype.clear = function () {
this.items = {}
}
// size 方法
Set.prototype.size = function () {
return Object.keys(this.items).length
}
// values 方法
Set.prototype.values = function () {
return Object.keys(this.items)
}
}
常见方法
- add(value):向集合添加一个新的项
- remove(value):从集合移除一个值
- has(value):如果值在集合中,返回 true,否则返回 false
- clear():移除集合中的所有项
- size():返回集合所包含的元素数量
- values():返回一个包含集合中所有值的数组
2.2.2 字典
保存一个人的信息
- 数组方式:[18, ‘zhangsan’, 1.80] ,可以通过下标值访问
- 字典方式:{‘age’: 18, ‘name’: ‘zhangsan’, ‘height’: 1.80} ,可以通过 key 取出 value
字典中的 key 不可以重复,而 value 可以重复,且 key 是 无序的
2.2.3 哈希表
概念
- 哈希化:将大数字转化成数组范围内下标的过程
- 哈希函数:通常将单词转成大数字,大数字在进行哈希化的代码实现放在一个函数中,这个函数为哈希函数
- 哈希表:最终将数据插入到的这个数组,对整个结构的封装,称之为一个哈希表
优缺点
基于数组实现,相对于数组有以下优缺点:
优点
- 非常快速的 插入-删除-查找 操作。无论多少数据,插入和删除需要接近常量的时间:即 O(1)
- 速度比树还快,基本可以瞬间查找到想要的元素
- 相对于树来说编码容易很多
缺点
- 没有顺序,不能以一种固定的方式(比如从小到大)来遍历其中的元素
- key 不允许重复
冲突
经过哈希化后的单词得到的下标是相同的
哈希函数实现
/***
* @param {str} 字符串
* @param {size} 数组范围大小
* @return {number}
*/
function hashFunc(str, size) {
let hashCode = 0
for (let i = 0; i < str.length; i++) {
hashCode = 37 * hashCode + str.charCodeAt(i)
}
const index = hashCode % size
return index
}
哈希表实现
function HashTable() {
// 属性
this.storage = []
this.count = 0
this.limit = 7
// 哈希函数
HashTable.prototype.hashFunc = function(str, size) {
let hashCode = 0
for (let i = 0; i < str.length; i++) {
hashCode = 37 * hashCode + str.charCodeAt(i)
}
const index = hashCode % size
return index
}
// 插入&修改操作
HashTable.prototype.put = function(key, value) {
// 1.根据 key 获取对应的 index
const index = this.hashFunc(key, this.limit)
// 2.根据 index 取出对应的 bucket
const bucket = this.storage[index] || []
// 3.判断是否是修改数据
for (let i = 0; i < bucket.length; i++) {
const tuple = bucket[i]
if (tuple[0] === key) {
tuple[1] = value
return
}
}
// 4.进行添加操作
bucket.push([key, value])
this.count++
// 5.判断是否需要扩容
if (this.count > this.limit * 0.75) {
this.resize(this.limit * 2)
}
}
// 获取操作
HashTable.prototype.get = function(key) {
// 1.根据 key 获取对应的 index
const index = this.hashFunc(key, this.limit)
// 2.根据 index 取出对应的 bucket
const bucket = this.storage[index]
// 3.判断是否存在 bucket
if (!bucket) return undefined
// 4.线性查找
for (let i = 0; i < bucket.length; i++) {
const tuple = bucket[i]
if (tuple[0] === key) {
return tuple[1]
}
}
// 5.没有找到,返回 undefined
return undefined
}
// 删除操作
HashTable.prototype.remove = function(key) {
// 1.根据 key 获取对应的 index
const index = this.hashFunc(key, this.limit)
// 2.根据 index 取出对应的 bucket
const bucket = this.storage[index]
// 3.判断是否存在 bucket
if (!bucket) return
// 4.线性查找
for (let i = 0; i < bucket.length; i++) {
const tuple = bucket[i]
if (tuple[0] === key) {
bucket.splice(i, 1)
this.count--
// 缩小容量
if (this.limit > 7 && this.count < this.limit * 0.25) {
this.resize(Math.floor(this.limit / 2))
}
return tuple[1]
}
}
}
// 扩容&缩容操作
HashTable.prototype.resize = function(newLimt) {
// 1.保存旧的数组内容
const oldStorage = this.storage
// 2.重置所有属性
this.storage = []
this.count = 0
this.limit = 7
// 3.遍历 oldStorage 中的所有 bucket
for (let i = 0; i < oldStorage.length; i++) {
// 3.1 取出对应的 bucket
const bucket = oldStorage[i]
// 3.2 判断 bucket 是否为空
if (!bucket) continue
// 3.3 遍历 bucket,取出数据,重新插入
for (let j = 0; j < bucket.length; j++) {
const tuple = bucket[j]
this.put([tuple[0], tuple[1]])
}
}
}
}
为什么需要扩容?
- 上面哈希表数据项放在长度为7的数组中的
- 随着数据的增多,每个index对应的bucket会越来越长,造成效率降低
- 所以需要按需扩容
三、树(Tree)
3.1 二叉树
树中每个节点最富哦只能有两个节点,这样的树称为"二叉树"
定义
- 二叉树可以为空,也就是没有节点
- 若不为空,则它是由根节点和称为其左子树TL和右子树TR的两个不相交的二叉树组成
五种形态
3.2 二叉搜索树
也称二叉排序树或二叉查找树,查找效率非常高
实现
function BinarySearchTree() {
function Node(key) {
this.key = key
this.left = null
this.right = null
}
// 根属性
this.root = null
BinarySearchTree.prototype.insertNode = function(node, newNode) {
if (newNode.key < node.key) { // 向左查找
if (node.left) {
this.insert(node.left, newNode)
} else {
node.left = newNode
}
} else { // 向右查找
if (node.right) {
this.insert(node.right, newNode)
} else {
node.right = newNode
}
}
}
// 插入
BinarySearchTree.prototype.insert = function(key) {
// 1.根据 key 创建节点
let newNode = new Node(key)
// 2.判断节点是否有值
if (this.root) {
this.insertNode(this.root, newNode)
} else {
this.root = newNode
}
}
// 最大值
BinarySearchTree.prototype.max = function() {
// 1.获取根节点
let node = this.root
// 2.依次向右不断查找,知道节点为 null
let key = null
while (node) {
key = node.key
node = node.right
}
return key
}
// 最小值
BinarySearchTree.prototype.max = function() {
// 1.获取根节点
let node = this.root
// 2.依次向右不断查找,知道节点为 null
let key = null
while (node) {
key = node.key
node = node.left
}
return key
}
// 搜索某一个 key
BinarySearchTree.prototype.search = function(key) {
// 1.获取根节点
let node = this.root
// 2.循环搜索 key
while(node) {
if (key < node.key) {
node = node.left
} else if (key > node.key) {
node = node,right
} else {
return true
}
}
return false
}
}
常见方法
- insert(key):向树中插入一个新的键
- search(key):在树中查找一个键,如果节点存在返回 true;不存在则返回 false
- min: 返回树中最小的值/键
- max: 返回树中最大的值/键
- remove(key): 从树中移除某个键
缺陷
- 插入连续数据后,分布不均匀,称为非平衡树
- 对于一棵平衡二叉树来说,插入/查找等操作的效率是O(logN)
- 对于一棵非平衡二叉树,相当于编写了一个链表,查找效率变成了O(N)
如有一棵初始化为 9 8 12的二叉树,插入下面的数据:7 6 5 4 3
3.3 红黑树
特性:
- 节点是红色或黑色
- 根节点是黑色
- 每个叶子节点都是黑色的空节点(NIL节点)
- 每个红色节点的两个节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
- 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点
关键特性:从根到叶子的最长可能路径,不会超过最短可能路径的两倍长,在最坏的情况下,依然是高效的
四、图
- 图结构是一种与树结构有些相似的数据结构
- 图论是数学的一个分支,并且在数学的概念上,树是图的一种
- 以图为研究对象,研究顶点和边组成的图形的数学理论和方法
- 主要研究目的是事物之间的关系,顶点代表事物,边代表两个事物间的关系
4.1 图的术语
- 顶点:比如地铁站中的某个站/多个村庄的某个村庄/互联网中的某台主机/人际关系中的人
- 边:顶点和顶点之间的连线
- 度:一个顶点的度是相邻顶点的数量
- 路径:顶点 v1、v2…,vn 的一个连续序列,比如上图中 0 1 5 9 就是一条路径
- 简单路径:要求不包含重复的顶点,比如 0 1 5 9 是一条简单路径
- 回路:第一个顶点和最后一个顶点相同的路径称为回路,比如 0 1 5 6 3 0
五、算法
5.1 大O表示法
描述计算机算法的效率
表示形式
5.2 冒泡排序
效率较低,但在概念上是排序算法中最简单的
思路:
- 对末排序的各元素从头到尾依次比较相邻的两个元素大小关系
- 如果左边的元素高,则两元素交换位置
- 向右移动一个位置,比较下面两个元素
- 当走到最右边时,最高的元素一定放在了最右边
- 按这个思路,从最左端重新开始,走到倒数第二个位置的元素即可
- 依次类推,就可以将数据排序完成
function bubblesort(arr) {
const length = arr.length
// j 每遍历一次就会把最大的放右边
for (let j = length - 1; j >= 0; j--) {
for (let i = 0; i < j; i++) {
if (arr[i] > arr[i + 1]) {
const tmp = arr[i]
arr[i] = arr[i + 1]
arr[i + 1] = tmp
}
}
}
return arr
}
console.log(bubblesort([1,441, 2, 15, -4])) // [-4, 1, 2, 15, 441]
效率
冒泡排序用大O表示法为O(n^2)
5.3 选择排序
时间复杂度跟冒泡排序一样,只不过减少了交换次数
思路:
- 选定第一个索引位置,然后和后面元素依次比较
- 如果后面的元素小于第一个索引位置的元素,则交换位置
- 经过一轮比较后,确定第一个位置是最小的
- 然后使用同样的方法把剩下的元素逐个比较即可
- 总结,第一轮选出最小值,第二轮选出第二小的值,直到最后
function selectionSort(arr) {
const length = arr.length
// 外层循环:从 0 位置开始取数据
for (let j = 0; j < length - 1; j++) {
// 内层循环:从 i+1 位置开始和后面的数据进行比较
let min = j
for (let i = min + 1; i < length; i++) {
if (arr[min] > arr[i]) min = i
}
// 调换位置
if (arr[min] < arr[j]) {
const tmp = arr[min]
arr[min] = arr[j]
arr[j] = tmp
}
}
return arr
}
console.log(selectionSort([1, 441, 2, 15, -4])) // [-4, 1, 2, 15, 441]
5.4 插入排序
局部有序:
- 在一个队列中,选择其中一个作为标记的元素
- 这个被标记的元素左边的所有元素已经是局部有序的
- 就是说,一部分是按顺序排列好的,一部分还没有顺序
思路:
- 从第一个元素开始,该元素可以认为已经被排序
- 取出下一个元素,在已经排序的元素序列中移到下一个位置
- 如果该元素(已排序)大于新元素,将该元素移到下一位置
- 重复以上步骤,直到找到已排序的元素小于或者等于新元素的位置
- 将新元素插入到该位置后,重复上面的步骤
function insertionSort(arr) {
const length = arr.length
// 外层循环:从 1 位置开始取数据,向前面局部有序进行插入
for (let i = 1; i < length; i++) {
const tmp = arr[i]
// 内层循环:从 i 位置开始取数据,和前面的数据依次进行比较
let j = i
while (arr[j - 1] > tmp) {
arr[j] = arr[j - 1]
arr[j - 1] = tmp
j--
}
}
return arr
}
console.log(insertionSort([1, 441, 2, 15, -4])) // [-4, 1, 2, 15, 441]
5.5 希尔排序
插入排序的一种高效的改进版,并且效率比插入排序要更快
插入排序的问题:
- 假设一个很小的数据项在很靠近右端的位置上,这里本来应该是较大的数据项的位置
- 把这个小数据项移动到左边的正确位置,所有的中间数据项都必须向右移动一位
- 如果每个步骤对数据项都进行N次复制,平均下来是移动 N/2,N个元素就是 N*N/2 = N^2/2
- 所以通常认为插入排序的效率是O(N^2)
- 如果有某种方式,不需要一个个移动所有中间的数据项,就i能把较小的数据项移动到左边,那么这个算法的执行效率就会有很大改进
思路:
比如一组数字:81,94,11,96,12,35,17,95,28,58,41,75,15
- 不正确的分组方式(81, 94, 11)(96, 12, 35)(17, 95, 28)(58, 41, 75)(15)
- 先让间隔为5进行排序.(35, 81)(94, 17)(11, 95)(96, 28)(12, 58)(35, 41)(17, 75)(95, 15)
- 排序后的新序列,一定可以让数字离自己的正确位置更近一步
- 再让间隔位3,进行排序.(35, 28, 75, 58, 95)(17, 12, 15, 81)(11, 41, 96, 94)
- 排序后的新序列,一定可以让数字离自己的正确位置又近了一步
- 最后,让间隔为1,也就是正确的插入排序
合适的增量:
- 在希尔排序的原稿中,初始间距是N/2,简单的把每趟排序分成两半
- 也就是说,对于 N = 100的数组,增量间隔序列为:50,25,12,6,3,1
- 这个方法的好处是不需要在开始排序前为合适的增量而进行任何的计算
function shellSort(arr) {
const length = arr.length
// 初始化增量
let gap = Math.floor(length / 2)
while (gap >= 1) {
// 以 gap 为间隔进行分组,对分组进行插入排序
for (let i = gap; i < length; i++) {
const tmp = arr[i]
let j = i
while (arr[j - gap] > tmp) {
arr[j] = arr[j - gap]
j -= gap
}
arr[j] = tmp
}
gap = Math.floor(gap / 2)
}
return arr
}
console.log(shellSort([81, 94, 11, 96, 12, 35, 17, 95, 28, 58, 41, 75, 15])) // [11, 12, 15, 17, 28, 35, 41, 58, 75, 81, 94, 95, 96]
效率:
- 希尔排序的效率跟增量是有关系的
- 但是它的效率证明非常困难,甚至某些增量的效率到目前依然没有被证明出来
- 最坏的情况下时间复杂度为O(N^2),通常情况下都要好于O(N^2)
5.6 快速排序
所有排序算法中,最快的一种排序算法
思想:
- 从其中选出了 65(可以是任意数字)
- 通过算法,将所有小于 65 的数字放在 65 的左边,将所有大于 65 的数字放在 65 的右边
- 递归处理左边的数据(比如选择 31 来处理左侧),递归处理右边的数据(比如选择 81 来处理右侧)
- 最终完成排序
枢纽选择方案
取头、中、尾的中位数
- 例如 8、12、3的中位数就是 8
平均效率是 O(N * logN)
function quickSort(arr) {
if (arr.length <= 1) {
return arr;
}
const pivot = arr[0]; // 选择第一个元素作为基准
const left = [];
const right = [];
for (let i = 1; i < arr.length; i++) {
if (arr[i] < pivot) {
left.push(arr[i]); // 比基准小的放在左边
} else {
right.push(arr[i]); // 比基准大的放在右边
}
}
return [...quickSort(left), pivot, ...quickSort(right)];
}
const unsortedArray = [6, 3, 8, 5, 2, 7, 4, 1];
const sortedArray = quickSort(unsortedArray);
console.log(sortedArray);