数组
概述
数组时占用一整块的存储空间的数据结构,一般在编程语言中都会内置数组。
数组是线性数据结构。
特性:
- 数组在物理空间(磁盘)上是连续的。
- 底层数组长度不可变。(我们对数组的操作,其实是js引擎帮我们完成的一系列操作)
- 数组变量指向第一个元素的位置(数组下标从 0 开始)
// 比如一个数组为 [1, 2, 3, 4, 5]
let arr = [1, 2, 3, 4, 5]
// arr 在内存中占用一整块空间
// arr相对于自己偏移量为 0 也就是自己本身的数据是 1
// arr相对于自己偏移量为 1 的数据是 2
// ... 就这样一直存储下去,占用了一整块空间。
优点:
- 查询性非常好。
缺点:
- 空间必须是连续的。当数组比较大,磁盘的空间碎片比较多的时候,容易存不下。不能高效利用空间碎片。
- 由于数组的长度是固定的,所以数组的内容难以被添加和删除。添加和移除的性能消耗比较大。
创建数组
// 1. 通过字面量创建数组
let array = []
let array2 = ["test", 1, true]
// 2. 通过数组的构造函数创造
let array3 = new Array()
let array4 = new Array("test", 2, false)
// 3. 基本操作
console.log(array2[0])
// > "test"
array2[array2.length] = "new"
console.log(array2)
// > Array ["test", 1, true, "new"]
console.log(array2 instanceof Array)
// > true
数组遍历
let arr = [1, 2, 3, 4, 5]
// 遍历数组函数
function travel(arr, fn) {
for (let i = 0; i < arr.length; i++) {
fn(arr[i], i, arr)
}
}
// 使用函数方法
travel(arr, (item, index, arr) => {
console.log(`travel: ${index}:${item}`)
})
// 递归遍历
function travel(arr, i) {
if (!arr[i]) return
console.log(arr[i])
travel(arr, i + 1)
}
// 将遍历数组函数添加到 Array.prototype 上,手写 forEach
Array.prototype.travel = function (fn) {
for (let i = 0; i < this.length; i++) {
fn ? fn(this[i], i, this) : console.log(this[i])
}
}
// 使用 Array 方法
arr.travel((item, index, arr) => {
console.log(`arr.travel: ${index}:${item}`)
})
arr.travel()
排序算法
本文排序算法共用的比较和交换算法
// 比较
function compare(a, b) {
return a > b ? true : false
}
// 交换
function exchange(arr, i, j) {
let temp = arr[i]
arr[i] = arr[j]
arr[j] = temp
}
冒泡排序
// 排序
function sort(arr) {
while (true) {
let exchanged = false
for (let i = 0; i < arr.length - 1; i++) {
if (compare(arr[i], arr[i + 1])) {
exchange(arr, i, i + 1)
exchanged = true
}
}
if (!exchanged) break
}
}
let arr = [5, 3, 2, 4, 5, 7, 9, 8, 0]
sort(arr)
console.log(arr)
选择排序
// 选择排序
function selectSort(arr) {
for (let i = 0; i < arr.length; i++) {
let minIndex = i
for (let j = i + 1; j < arr.length; j++) {
if (compare(arr[minIndex], arr[j])) minIndex = j
}
exchange(arr, i, minIndex)
}
}
let arr = [4, 1, 6, 5, 3, 2, 8, 7]
selectSort(arr)
console.log(arr)
插入排序
// 插入排序
function insertSort(arr) {
for (let i = 1; i < arr.length; i++) {
//外循环从1开始,默认arr[0]是有序段
let mark = i
for (let j = i - 1; j >= 0; j--) {
if (arr[i] >= arr[j]) break
mark = j
}
let temp = arr.splice(i, 1)[0]
arr.splice(mark, 0, temp)
}
}
let arr = [3, 3, 2, 1, 3, 8, 7, 6, 9, 5, 4]
insertSort(arr)
console.log(arr)
简单快速排序
上面快速排序的青春版。
// 简单快速排序
function simpleQuickSort(arr) {
if (arr.length < 2) return arr
let left = []
let right = []
let base = arr[0]
for (let i = 1; i < arr.length; i++) {
if (arr[i] < base) {
left.push(arr[i])
} else {
right.push(arr[i])
}
}
left = simpleQuickSort(left)
right = simpleQuickSort(right)
left.push(base)
return left.concat(right)
}
let arr = [3, 3, 2, 1, 3, 8, 7, 6, 9, 5, 4]
simpleQuickSort(arr)
console.log(simpleQuickSort(arr))
标准快速排序
// 快速排序
function quickSort1(arr, begin, end) {
// 递归的退出条件
if (begin >= end) return
if (begin + 1 == end) {
if (arr[begin] > arr[end]) exchange(arr, begin, end)
return
}
// 以数组中最后一位为基准
let base = end
// 左指针从数组头元素开始
let left = begin
// 右指针从基准前一个元素开始
let right = end - 1
// 只要左右指针没有合并或越界,让其中一直循环,直至左右合并或越界
while (left < right) {
// 从左指针开始,当左指针所指元素小于基准,那么让左指针右移,当此循环停止时,左指针所指元素大于基准
while (left < right && arr[left] < arr[base]) {
left++
}
// 如果左指针一直没有找到,直到两个指针重合,停止两指针的查找,跳出循环将指针重合的元素与基准替换。
if (left == right) break
// 左指针停止后,开始右指针的查找,当右指针所指元素大于基准,让右指针左移,当此循环停止时,右指针所指元素小于基准
// 此处取等是为了避免,左指针所指元素和右指针所指元素和基准元素的值相等,出现死循环。
while (left < right && arr[right] >= arr[base]) {
right--
}
// 当在左右指针重合前,都找到了符合标准的元素,将这两个元素替换,继续重复查找,直至两指针重合
if (left < right) exchange(arr, left, right)
}
// 此时左指针和右指针重合,让其所指元素与基准替换
exchange(arr, left, base)
// 此时将指针重合的上一个元素到开头,下一个元素到结尾重新划分数组继续上面的操作进行递归。指针重合后,替换到这里的基准元素后续不进行操作。
quickSort1(arr, begin, left - 1)
quickSort1(arr, left + 1, end)
}
function quickSort(arr) {
quickSort1(arr, 0, arr.length - 1)
}
let arr = [3, 3, 2, 1, 3, 8, 7, 6, 9, 5, 4]
quickSort(arr)
console.log(arr)
归并排序
// 归并排序
function mergeSort(arr) {
if (arr.length == 1) return arr
let mid = Math.floor(arr.length / 2)
let left = arr.slice(0, mid)
let right = arr.slice(mid)
left = mergeSort(left)
// console.log("left:", left)
right = mergeSort(right)
// console.log("right", right)
let res = []
while (left.length + right.length) {
if (left[0] <= right[0] || right.length == 0) {
res.push(left.shift())
} else {
res.push(right.shift())
}
// res.push(left[0] <= right[0] ? left.shift() : right.shift())
}
return res
}
let arr = [3, 3, 2, 1, 3, 8, 7, 6, 9, 5, 4]
console.log(mergeSort(arr))
注意
我们向函数传递形参时,直接修改形参,不会对实参起到修改作用。但是如果修改形参内部的值则会起作用。如形参传递数组或对象时,修改其中的元素和属性将会对实参起作用,但是如果修改整个数组,则对实参不会起作用
// 不会对实参arr进行修改
function changeArr(arr){
arr = 1
}
// 会对实参arr进行修改
function changeArr2(arr){
arr[0] = 1
}
链表
概述
链表是带有封装性质的一种数据结构。是多个 node 而组成的一个数据链,称为链表。
链表是线性数据结构。
node 包括(多个 node 组成链表):
- 数据。
- 引用(指针)。
特点:
- 链表由于有许多 node 组成,所以空间不连续。可以高效利用碎片空间。
- 每个 node 中都有一个指针指向下一个 node,这样多个 node 组成了一个 链表。
优点:
- 只要内存足够大就能存的下,不需要一整块内存,高效利用碎片空间。
- 链表可以非常容易添加和删除。
缺点:
- 链表的查询速度慢,因为链表中没有索引,只能一层一层从头向尾查找。
- 由于一个node中既要有数据,又要有引用值所以多占用了空间。如果数据值占用小,那么引用值就显得臃肿。如果数据值占用大,那么引用值相对于数据值就微不足道了。所以适合大数据存储。
链表使用:
- 我们在使用链表时,只需要知道根节点,就知道了一整个链表。因为根节点可以根据其中的引用直接查找到最后。
- 每一个节点都认为自己是根节点,因为都可以从自己查到链表尾。
链表的实现
function Node(value) {
this.value = value
this.next = null
}
let a = new Node(1)
let b = new Node(2)
let c = new Node(3)
let d = new Node(4)
a.next = b
b.next = c
c.next = d
d.next = new Node(5)
console.log(a.next.next)
/* out:
Node {
value: 3,
next: Node { value: 4, next: Node { value: 5, next: null } }
}
*/
遍历链表
// 遍历链表
function travel(node){
while(node){
console.log(node.value)
node = node.next
}
}
// 递归遍历
function travel(node) {
if (!node) return
console.log(node.value)
travel(node.next)
}
travel(a)
链表的逆置
原理图解:

function Node(value) {
this.value = value
this.next = null
}
let node1 = new Node(1)
let node2 = new Node(2)
let node3 = new Node(3)
let node4 = new Node(4)
node1.next = node2
node2.next = node3
node3.next = node4
function travel(node) {
if (!node) return
console.log(node.value)
travel(node.next)
}
// 递归
function reverse(root) {
if (!root.next.next) {
// console.log(root.value)
root.next.next = root
return root.next
} else {
let res = reverse(root.next)
// console.log(root.value)
root.next.next = root
root.next = null
return res
}
}
let final = reverse(node1)
travel(final)
栈和队列
栈
栈就像弹夹,先进去的子弹最后打出,最后上的子弹先打出。
// 栈
function Stack() {
this.value = []
this.enter = function (value) {
this.value.push(value)
}
this.out = function () {
return this.value.pop()
}
}
let stack = new Stack()
stack.enter(1)
stack.enter(2)
stack.enter(3)
console.log(stack.value)
console.log(stack.out())
console.log(stack.value)
队列
队列是先进去的先出来,后进去的后出来。
// 队列
function Queue() {
this.value = []
this.enter = function (value) {
this.value.push(value)
}
this.out = function () {
return this.value.shift()
}
}
let queue = new Queue()
queue.enter(1)
queue.enter(2)
queue.enter(3)
console.log(queue.value)
console.log(queue.out())
console.log(queue.value)