数据结构 - 01

95 阅读9分钟

图灵得主尼古拉斯·沃斯曾经说过算法+数据结构=程序

数据结构和算法是计算机科学的基础,在设计和分析程序的时候起到了重要作用,正确的使用数据结构和算法可以提高程序的性能和可维护性

数据结构

  • 数据结构就是在计算机中,存储和组织数据的方式
  • 常见的数据结构有数组,链表,栈,队列,树,图...等等
  • 用一个优秀的数据结构,能够更快的帮你解决问题,优化程序性能

1.线性结构

  • 是由n个元素组成的a[0],a[1],a[2]...a[n-1]组成的有序数列
    1. 数组是一个线性结构
    2. 链表是一个线性结构
    3. 栈是一个受限的线性结构
    4. 队列是一个受限的线性结构

接下来用代码实现一个栈结构

// 栈结构 : 后进入先出
interface IStack<T> {
  push: (el: T) => void
  pop: () => void
  peek: () => T | undefined
  isEmpty: () => boolean
  size: () => number
}
class DJArrayStack<T> implements IStack<T> {
  private data: T[] = []

  push(element: T) {
    this.data.push(element)
  }

  pop() {
    return this.data.pop()
  }
  peek(): T | undefined {
    if (this.data.length) {
      return this.data[this.data.length - 1]
    }
    return undefined
  }
  isEmpty(): boolean {
    return !this.data.length
  }
  size(): number {
    return this.data.length
  }
}

栈结构相关面试题

  1. 十进制转二进制
function decimalToBinary(num: number): string {
  // 4 =>  100
  if (num == 0) {
    return '0'
  }

  const arr = new DJArrayStack<number>()
  while (num > 0) {
    arr.push(num % 2)
    num = Math.floor(num / 2)
  }
  let res = ''
  while (!arr.isEmpty()) {
    res += arr.pop()
  }
  return res
}
  1. 有效的括号
const parentheses = '[]({})'
function checkParentheses(par: string): boolean {
  const stack = new DJArrayStack<string>()
  for (let i = 0; i < par.length; i++) {
    const element = par[i]
    switch (element) {
      case '(':
        stack.push(')')
        break
      case '[':
        stack.push(']')
        break
      case '{':
        stack.push('}')
        break
      default:
        if (element != stack.pop()) return false
        break
    }
  }

  return stack.isEmpty()
}

2.队列结构

  • 先进先出
  • node的任务队列
class DJArrayQueue<T> {
  private data: T[] = []
  enqueue(...args: T[]) {
    this.data.push(...args)
  }
  dequeue(): T | undefined {
    return this.data.shift()
  }
  peek(): T | undefined {
    if (this.data.length == 0) {
      return undefined
    }
    return this.data[0]
  }
  get isEmpty() {
    return !this.data.length
  }
  get size() {
    return this.data.length
  }
}

相关面试题

  1. 击鼓传花
const names = ['0', '1', '2', '3', '4']
function getJgchLastIndex(names: string[], outIndex: number) {
  const queue = new DJArrayQueue<string>()
  queue.enqueue(...names)

  let i = 0
  while (queue.size > 1) {
    i++
    let front = queue.dequeue()
    if (i == outIndex) {
      i = 0
    } else {
      queue.enqueue(front)
    }
  }
  return queue.peek()
}
  1. 约斯夫环 用动态规划
function lastRemaining(n: number, m: number): number {
  let ans = 0

  return ans
}

3. 链表的实现
  • 链表分为单向链表和双向链表
class DJNode<T> {
  value: T
  next: DJNode<T> | null = null
  constructor(val: T) {
    this.value = val
  }
}
class DJLinkedList<T> {
  head: DJNode<T> | null = null
  private size: number = 0
  private _lastNode: DJNode<T> | null = null
  get length() {
    return this.size
  }
  /**向链表尾部添加一个新的项 */
  append(element: T) {
    const newNode = new DJNode<T>(element)
    if (this.size == 0) {
      this.head = newNode
    } else {
      this._lastNode.next = newNode
    }
    this._lastNode = newNode
    this.size++
  }
  /**向链表的特定位置插入一个新的项 */
  insert(position: number, element: T) {
    const newNode = new DJNode<T>(element)
    if (position == 0 || this.size == 0) {
      newNode.next = this.head
      this.head = newNode
    } else if (position >= this.size) {
      this._lastNode.next = newNode
      this._lastNode = newNode
    } else {
      const preNode = this.getNode(position - 1)
      newNode.next = preNode.next
      preNode.next = newNode
      this.size++
    }
  }
  /**获取对应位置的元素 */
  get(position: number): T | undefined {
    const curNode = this.getNode(position)
    return curNode.value
  }
  /**返回元素所在链表中的索引,如果链表中没有该元素则返回-1 */
  indexOf(element: T) {
    if (this.size == 0) {
      return -1
    }
    let index = 0
    let curNode = this.head

    while (curNode.next) {
      if (element === curNode.value) {
        break
      }
      index++
      curNode = curNode.next
    }
    return index
  }
  /**更新某个位置的元素 */
  update(position: number, element: T) {
    const ans = this.getNode(position)
    if (ans != null) {
      ans.value = element
    }
  }
  /**移除特定位置 */
  removeAt(position: number) {
    if (position >= this.size) {
      return
    }
    if (position == 0) {
      this.head = this.head.next
    }
    const preNode = this.getNode(position - 1)
    preNode.next = preNode.next.next
    this.size--
  }
  /**移除特定元素*/
  remove(element: T) {
    const position = this.indexOf(element)
    if (position >= 0) {
      this.removeAt(position)
    }
  }
  isEmpty() {
    return this.size > 0
  }

  /**获取特定位置的Node */
  private getNode(position: number): DJNode<T> | null {
    let index = 0
    let curNode = this.head
    while (index++ < position && curNode) {
      curNode = curNode.next
    }
    return curNode
  }

  /**打印 */
  traverse() {
    let curNode = this.head
    let res = ''
    while (curNode) {
      res += curNode.value + ' -> '
      curNode = curNode.next
    }
    console.log(res)
  }
}

4. 哈希表
  • 本质就是将下标值先哈希化
  • 哈希表的实现还是利用了数组
  • 解决下标值问题
    • 1.链地址法解析
     链地址法解决冲突的办法是每个数组单元中存储的不再是单个数据,而是一个链条
         *  这个链条使用的数据结构通常是数组或者是链表
         *  如果是链表, 就是每个数组的单元中存储一个链条,一旦有重复,则把重复的元素加入到该链条的尾部或者首部
         *  当查询时,先根据哈希化后的下标值找到对应的位置,再取出链表,依次查询寻找的数据
    
         那选择数组还是链表呢?
            *   数组或者链表这里都可以,效率上是差不多的
            *   因为根据哈希化的index找到这个数组或者链表的时候,通常就会使用线性查找,这个时候数组和链表的效率差不多
            *   当然在某些实现中,会将新插入的数据放在数组或链表的最前面,因为觉得新插入的数据取出的概率更大
            *   这种情况最好是用链表,因为插入数据链表的O(1)比数组O(n)要好
            *   总之还是看业务需求,这里数组或链表都是可以的
    
    • 2.开放地址法
       开发地址的主要做法是寻找空白的位置来添加重复的数据
        *  解决重复index又有三种方法
        *  1.线性探测
        *  2.二次探测
        *  3.再哈希
      
  • 链地址法效率是要好于开放地址法的
  • Java的HashMap就是用的链地址法

核心是哈希函数 -> 而哈希函数的实现使用霍纳法则来减少生成hashCode所需要经历的乘法+加法,主要是乘法的减少,乘法对比加减法还是比较消耗性能

代码实现:

/**
 * 这个函数返回这个key在数组中的下标值
 * @param key 要保存的key
 * @param max 哈希表中数组的长度
 */
function hashFunc(key: string, max: number): number {
  let hashCode = 0
  const length = key.length
  for (let i = 0; i < length; i++) {
    hashCode = 31 * hashCode + key.charCodeAt(i)
  }
  let index = hashCode % max
  return index
}
//哈希表
class HashTable<T = any> {
  storage: [string, T][][] = []
  private length = 7
  private count = 0

  private hashCode(key: string, max: number): number {
    return hashFunc(key, max)
  }

  put(key: string, value: T) {
    //获取索引值
    const index = this.hashCode(key, this.length)

    let bucket = this.storage[index]
    if (!bucket) {
      bucket = []
      this.storage[index] = bucket
    }
    let isUpdate = false
    for (let i = 0; i < bucket.length; i++) {
      const tuple = bucket[i]
      const tupleKey = tuple[0]
      if (tupleKey === key) {
        tuple[1] = value
        isUpdate = true
        break
      }
    }

    if (!isUpdate) {
      bucket.push([key, value])
      this.count++
      const loadFactor = this.count / this.length
      if (loadFactor > 0.75) {
        this.resize(this.length * 2)
      }
    }
  }

  get(key: string): T | undefined {
    const index = this.hashCode(key, this.length)
    const bucket = this.storage[index]
    if (!bucket) {
      return undefined
    }
    for (let i = 0; i < bucket.length; i++) {
      const [tupleKey, tupleValue] = bucket[i]
      if (tupleKey === key) {
        return tupleValue
      }
    }

    return undefined
  }
  delete(key: string): boolean {
    const index = this.hashCode(key, this.length)
    const bucket = this.storage[index]
    if (!bucket) {
      return false
    }
    for (let i = 0; i < bucket.length; i++) {
      const [tupleKey, _] = bucket[i]
      if (tupleKey === key) {
        bucket.splice(i, 1)
        this.count--
        const loadFactor = this.count / this.length

        if (loadFactor < 0.25) {
          this.resize(Math.floor(this.length / 2))
        }
        return true
      }
    }
    return false
  }

  private resize(newLength: number) {
    if (newLength < 7) newLength = 7
    if (this.length === newLength) return

    const primeNum = this.getNextPrimeNumber(newLength)

    console.log('新的容量 : ', primeNum)
    this.length = primeNum
    const oldStorage = this.storage
    this.storage = []
    this.count = 0

    oldStorage.forEach((bucket) => {
      if (!bucket) {
        return
      }
      for (let i = 0; i < bucket.length; i++) {
        const [key, value] = bucket[i]
        this.put(key, value)
      }
    })
  }
  // 获取下一个质数
  private getNextPrimeNumber(num: number): number {
    let newPrime = num
    while (!this.isPrimeNumber(newPrime)) {
      newPrime++
    }
    return newPrime
  }

  //判断是否质数
  private isPrimeNumber(num: number): boolean {
    if (num < 2) {
      return false
    }

    const mid = Math.sqrt(num)
    for (let i = num - 1; i >= mid; i--) {
      const remainder = num % i
      if (remainder === 0) {
        return false
      }
    }
    return true
  }
}

const map1 = new HashTable()
map1.put('cab', 111)
map1.put('nab', 222)
map1.put('nba', 333)
map1.put('cba', 444)
map1.put('ddd', 444)
map1.put('fff', 444)
map1.put('ggg', 444)
5. 树结构

所有的树结构本质上都可以使用二叉树把它表述出来的 - 因为用儿子-兄弟这个表示方法,然后旋转45度,然后就是一个二叉树

二叉树的特性

  1. 一颗二叉树的第i层的最大节点数为: 2^(i- 1), i>=1
  2. 深度为K的二叉树有最大的节点总数为: 2^k - 1, k>=1
  3. 对于任何非空二叉树T,若n0表示叶子节点的个数,n2表示度为2的非叶子节点个数,那么二者满足关系 n0 = n2 + 1

完美二叉树(也叫做满二叉树)

  • 假设深度为K,则除了第K层的叶子节点,其他节点都是度为2

完全二叉树 (堆的本质就是一颗完全二叉树)

  • 假设深度为K,则除了二叉树第K层外,其他节点都是度为2
  • 且最后一层从左往右的叶节点是连续存在,只缺右侧若干节点
  • 完美二叉树是特殊的完全二叉树

二叉树的存储

  • 数组: 完全二叉树可以用数组来存,非完全二叉树用数组存储需要先补全成完全二叉树,会造成内存的浪费

  • 链表: 比较常用

二叉搜索树BST

不用空的情况下,要满足下面条件

  • 非空左子树的所有键值小于其根节点的键值
  • 非空右子树的所有键值大于其根节点的键值
  • 左右子树本身也都是二叉搜索树

二叉搜索树 实现

//二叉搜索树 实现
class TreeNode<T> {
  value: T | null = null
  left: TreeNode<T> | null = null
  right: TreeNode<T> | null = null
  parent: TreeNode<T> | null = null
  constructor(value: T) {
    this.value = value
  }
  get isLeft(): boolean {
    return this.parent && this.parent.left === this
  }
  get isRight(): boolean {
    return this.parent && this.parent.right === this
  }
}
class BSTree<T> {
  private root: TreeNode<T> | null = null
  insert(value: T) {
    const node = new TreeNode<T>(value)
    if (!this.root) {
      this.root = node
      return
    }
    let parentNode = this.root
    while (parentNode) {
      if (node.value > parentNode.value) {
        //找右边
        if (parentNode.right == null) {
          parentNode.right = node
          break
        }
        parentNode = parentNode.right
      } else {
        //找左边
        if (parentNode.left == null) {
          parentNode.left = node
          break
        }
        parentNode = parentNode.left
      }
    }
  }

  /**
   * 前序遍历
   *  - 先访问根节点
   *  - 再访问左子树
   *  - 再访问右子树
   */
  preOrderTraverse() {
    if (!this.root) {
      return
    }

    const ans = []

    function traverse(node: TreeNode<T>) {
      if (!node) {
        return
      }
      ans.push(node.value)
      traverse(node.left)
      traverse(node.right)
    }
    traverse(this.root)
    console.log(ans)
  }
  /**
   * 中序遍历
   *  - 先访问左子树
   *  - 再访问根节点
   *  - 再访问右子树
   */
  inOrderTraverse() {
    if (!this.root) {
      return
    }
    const ans = []
    function traverse(node: TreeNode<T>) {
      if (!node) {
        return
      }
      traverse(node.left)
      ans.push(node.value)
      traverse(node.right)
    }
    traverse(this.root)
    console.log(ans)
  }
  /**
   * 后序遍历
   *  - 先访问左子树
   *  - 再访问右子树
   *  - 再访问根节点
   */
  postOrderTraverse() {
    if (!this.root) {
      return
    }
    const ans = []
    function traverse(node: TreeNode<T>) {
      if (!node) {
        return
      }
      traverse(node.left)
      traverse(node.right)
      ans.push(node.value)
    }
    traverse(this.root)
    console.log(ans)
  }
  /**
   * 层序遍历
   *  - 一层一层的遍历
   *  - 用队列来实现
   */
  levelOrderTraverse() {
    if (!this.root) {
      return
    }
    const ans = []
    const queue = new DJArrayQueue<TreeNode<T>>()
    queue.enqueue(this.root)
    while (!queue.isEmpty) {
      const node = queue.dequeue()
      ans.push(node.value)
      if (node.left) {
        queue.enqueue(node.left)
      }
      if (node.right) {
        queue.enqueue(node.right)
      }
    }

    console.log(ans)
  }

  getMaxValue(): T | null {
    if (!this.root) {
      return null
    }
    let node = this.root
    while (node.right) {
      node = node.right
    }
    return node.value
  }
  getMinValue(): T | null {
    if (!this.root) {
      return null
    }
    let node = this.root
    while (node.left) {
      node = node.left
    }
    return node.value
  }

  private searchNode(value: T): TreeNode<T> | null {
    let node = this.root
    while (node) {
      if (value === node.value) {
        return node
      }
      const parent = node
      if (value > node.value) {
        node = node.right
      } else if (value < node.value) {
        node = node.left
      }
      node.parent = parent
    }
    return null
  }
  // 搜索到特定的值
  search(value: T): boolean {
    //非递归
    // let node = this.root
    // while (node) {
    //   if (value > node.value) {
    //     node = node.right
    //   } else if (value < node.value) {
    //     node = node.left
    //   } else {
    //     return true
    //   }
    // }
    // return false

    //递归
    function findValue(node: TreeNode<T>): boolean {
      if (!node) {
        return false
      }
      if (value === node.value) {
        return true
      }
      if (value > node.value) {
        return findValue(node.right)
      } else {
        return findValue(node.left)
      }
    }
    return findValue(this.root)
  }

  //删除功能
  delete(value: T): boolean {
    //首先要找到这个元素
    const node = this.searchNode(value)
    if (!node) {
      return false
    }
    // console.log(`要删除的元素${node.value} - 父节点=${node.parent.value} - 是左边吗=${node.isLeft}`)
    //一共有3种情况
    //1.删除的是叶子节点
    if (node.left == null && node.right == null) {
      if (node === this.root) {
        this.root = null
      } else if (node.isLeft) {
        node.parent.left = null
      } else {
        node.parent.right = null
      }
    } else if (node.left == null) {
      //2.删除的节点 只有一个子节点
      if (node == this.root) {
        this.root = node.right
      } else if (node.isLeft) {
        node.parent.left = node.right
      } else {
        node.parent.right = node.right
      }
    } else if (node.right == null) {
      if (node == this.root) {
        this.root = node.left
      } else if (node.isLeft) {
        node.parent.left = node.left
      } else {
        node.parent.right = node.left
      }
    } else {
      //3.删除的节点 有2个子节点
      // 这个关键在于找到删除节点的前驱或者是后继节点
      // 1. 前驱: 就是比删除节点稍微小一点的数字
      // 2. 后继: 就是比删除节点稍微大一点的数字
      // 找前驱
      const preNode = findPreMax(node)
      if (node.left !== preNode) {
        preNode.parent.right = preNode.left
        preNode.left = node.left
      }

      preNode.right = node.right

      if (node === this.root) {
        this.root = preNode
      } else if (node.isLeft) {
        node.parent.left = preNode
      } else {
        node.parent.right = preNode
      }
    }

    function findPreMax(delNode: TreeNode<T>): TreeNode<T> {
      let preNode = delNode.left
      while (preNode.right) {
        const parent = preNode
        preNode = preNode.right
        preNode.parent = parent
      }
      return preNode
    }

    return true
  }

  print() {
    btPrint(this.root)
  }
}

const bstree = new BSTree<number>()
bstree.insert(30)
bstree.insert(22)
bstree.insert(24)
bstree.insert(26)
bstree.insert(23)
bstree.insert(33)
bstree.insert(13)
bstree.insert(6)
bstree.insert(16)
bstree.insert(15)
bstree.insert(34)
// bstree.print()

bstree.delete(30)
6. 图结构

历史: 欧拉和七桥问题

  • 图结构是一种与树结构有些相似的数据结构

  • 图论是数学的一个分支,并且在数学的概念上,树是图的一种

  • 它以图为研究对象,研究顶点和边组成的图形的数学理论和方法

  • 主要研究的目的是事物之间的关系, 顶点代表事物,边代表两个事物之前的关系

  • 历史: 欧拉和七桥问题

    • 连通图可以一笔画的必要条件
      1. 奇点的数目不是0个就是2个
      1. 想要一笔画成, 必须中间都是偶数点 也就是说: 有来路必有另一条去路,奇点只能在两端,因此任何图能一笔画成,奇点要么没有,要么在两端

图结构的表示 - 1.邻接矩阵 : - 是用二维数组来表示2个顶点之间有没有联系,有联系显示1 没联系显示0 - 但是这样在稀疏图的时候,用有大量的0来保存在内存中,浪费空间

- 2.邻接表
    - 邻接表由图中 每个顶点以及和顶点相邻的顶点列表组成
    - 每个列表有很多的方式来存储: 数组/链表/字典(哈希表)都可以

图的访问: 1.广度优先 BFS 基于队列 2.深度优先 DFS 基于栈或者使用递归

    class Graph<T> {
 //顶点
 private vertecs: T[] = []
 //路径
 private edges: Map<T, T[]> = new Map()

 //添加顶点
 addVertex(vertex: T) {
   this.vertecs.push(vertex)
   this.edges.set(vertex, [])
 }
 //添加路径
 addEdge(vertex: T, edge: T) {
   this.edges.get(vertex).push(edge)
 }
 traversal() {
   this.vertecs.forEach((vertex) => {
     const edges = this.edges.get(vertex)
     console.log(`顶点 ${vertex} 路径有${edges.join(' ')}`)
   })
 }

 //广度优先
 bfs() {
   //用队列实现
   const queue = new DJArrayQueue<T>()
   queue.enqueue(this.vertecs[0])

   const visited = new Set<T>()
   visited.add(this.vertecs[0])

   while (!queue.isEmpty) {
     const vertex = queue.dequeue()
     console.log(vertex)
     const neighbor = this.edges.get(vertex)
     if (!neighbor) continue
     neighbor.forEach((n) => {
       if (!visited.has(n)) {
         visited.add(n)
         queue.enqueue(n)
       }
     })
   }
 }
 //深度优先
 dfc() {
   //使用栈来实现
   const stack = new DJArrayStack<T>()
   stack.push(this.vertecs[0])
   const visited = new Set<T>()
   visited.add(this.vertecs[0])

   while (!stack.isEmpty) {
     const pop = stack.pop()
     console.log(pop)
     const neighbor = this.edges.get(pop)
     if (!neighbor) continue
     for (let i = neighbor.length - 1; i >= 0; i--) {
       if (!visited.has(neighbor[i])) {
         visited.add(neighbor[i])
         stack.push(neighbor[i])
       }
     }
   }
 }
}

const graph = new Graph()
for (let i = 65; i < 71; i++) {
 graph.addVertex(String.fromCharCode(i))
}
graph.addEdge('A', 'B')
graph.addEdge('A', 'C')
graph.addEdge('A', 'D')
graph.addEdge('C', 'D')
graph.addEdge('C', 'G')
graph.addEdge('D', 'G')
graph.addEdge('D', 'H')
graph.addEdge('B', 'E')
graph.addEdge('B', 'F')
graph.addEdge('E', 'I')

graph.traversal()   

算法

  • 算法就是指解决问题的方法和步骤
  • 常见的算法有快速排序,冒泡排序,二分查找,最短路径算法...等等

二分查找

// 二分查找
function binarySearch(arr: number[], num: number) {
  let left = 0
  let right = arr.length - 1
  while (left <= right) {
    let index = Math.floor((right - left) / 2 + left)
    if (num > arr[index]) {
      left = index + 1
    } else if (num < arr[index]) {
      right = index - 1
    } else {
      return index
    }
  }
  return -1
}