数据结构与算法

一、数据结构和算法

1.1 数据结构

数据结构是计算机中存储、组织数据的方式。比如图书馆里存放书籍,当需要查找某一本书籍时能以合理的方式迅速找到

1.2 算法(Olgorithm)

解决问题的办法/步骤逻辑。数据结构的实现离不开算法

生活中的算法:

拿快递:当报出名字跟号码时,快递小哥能迅速找到你的快递

方式:

  1. 线性查找

    一个一个比对,最终找到

  2. 二分查找

    从中间位置开始查找,先看一头有没有问题,如果没有则从另一头取中间点再次查找,以此类推

二、线性和非线性数据结构

线性结构:数据元素之间一对一的关系
非线性结构:数据元素之间一对多的关系

存储方式

顺序存储:Set、Array
散列存储:Map、Object
链式存储:链表

2.1 线性结构

2.1.1 数组

数组是一种线性结构,可以在数组的任意位置插入和删除数据

  1. JS 的数组就是API的调用
  2. 普通语言的数组封装(比如Java的ArrayList)
    • 不能存放不同的数据类型,通常在封装时存放在数组中的是 Object 类型
    • 容量不会自动改变(需要进行扩容处理)
    • 进行中间插入和删除操作性能比较低
    • 根据下标查找值效率较高,根据内容查找值效率较低

2.1.2 栈(stack)

栈是一种受限的线性表,先进后出(LIFO last in first out)

概念
  1. 仅允许在表的一端进行插入和删除运算
  2. LIFO(last in first out)表示后进入的元素,先弹出栈空间
  3. 向栈插入新元素又称作进栈、入栈或压栈,把新元素放到栈顶元素的上面,使之成为新的栈顶元素
  4. 向栈删除元素又称作出栈或退栈,先把栈顶元素删掉,其相邻的元素成为新的栈顶元素

生活中的栈:自助餐的托盘

应用
  1. A函数 调用 B函数,B函数又 调用 C函数
  2. 递归,不断调用自己
实现
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)进行插入操作
应用
  1. 打印队列
    • 有五份文档需要打印,按次序放入打印队列中
    • 打印机会依次从队列中取出文档,先放入的文档优先取出
    • 以此类推,直到队列中不再有新的文档
  2. 线程队列
    • 在开发中,为了让任务可以并行处理,通常会开启多个线程
    • 但是不能让大量的线程同时运行处理任务(占用过多的资源)
    • 这个时候可以使用线程队列,按次序启动线程,并且处理对应的任务
实现
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 红黑树

特性:

  1. 节点是红色或黑色
  2. 根节点是黑色
  3. 每个叶子节点都是黑色的空节点(NIL节点)
  4. 每个红色节点的两个节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
  5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点

关键特性:从根到叶子的最长可能路径,不会超过最短可能路径的两倍长,在最坏的情况下,依然是高效的

在这里插入图片描述

四、图

  • 图结构是一种与树结构有些相似的数据结构
  • 图论是数学的一个分支,并且在数学的概念上,树是图的一种
  • 以图为研究对象,研究顶点组成的图形的数学理论和方法
  • 主要研究目的是事物之间的关系顶点代表事物代表两个事物间的关系

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 快速排序

所有排序算法中,最快的一种排序算法

思想:

在这里插入图片描述

  1. 从其中选出了 65(可以是任意数字)
  2. 通过算法,将所有小于 65 的数字放在 65 的左边,将所有大于 65 的数字放在 65 的右边
  3. 递归处理左边的数据(比如选择 31 来处理左侧),递归处理右边的数据(比如选择 81 来处理右侧)
  4. 最终完成排序

枢纽选择方案

取头、中、尾的中位数

  • 例如 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);
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值