前端人的数据结构与算法

113 阅读4分钟

数组

概述

数组时占用一整块的存储空间的数据结构,一般在编程语言中都会内置数组。

数组是线性数据结构。

特性:

  1. 数组在物理空间(磁盘)上是连续的。
  2. 底层数组长度不可变。(我们对数组的操作,其实是js引擎帮我们完成的一系列操作)
  3. 数组变量指向第一个元素的位置(数组下标从 0 开始)
// 比如一个数组为 [1, 2, 3, 4, 5]
let arr = [1, 2, 3, 4, 5]
// arr 在内存中占用一整块空间
// arr相对于自己偏移量为 0 也就是自己本身的数据是 1
// arr相对于自己偏移量为 1 的数据是 2
// ... 就这样一直存储下去,占用了一整块空间。

优点:

  1. 查询性非常好。

缺点:

  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 组成链表):

  1. 数据。
  2. 引用(指针)。

特点:

  1. 链表由于有许多 node 组成,所以空间不连续。可以高效利用碎片空间。
  2. 每个 node 中都有一个指针指向下一个 node,这样多个 node 组成了一个 链表。

优点:

  1. 只要内存足够大就能存的下,不需要一整块内存,高效利用碎片空间。
  2. 链表可以非常容易添加和删除。

缺点:

  1. 链表的查询速度慢,因为链表中没有索引,只能一层一层从头向尾查找。
  2. 由于一个node中既要有数据,又要有引用值所以多占用了空间。如果数据值占用小,那么引用值就显得臃肿。如果数据值占用大,那么引用值相对于数据值就微不足道了。所以适合大数据存储。

链表使用:

  1. 我们在使用链表时,只需要知道根节点,就知道了一整个链表。因为根节点可以根据其中的引用直接查找到最后。
  2. 每一个节点都认为自己是根节点,因为都可以从自己查到链表尾。

链表的实现

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)