数据结构(Javascript)

101 阅读6分钟

递归

  • 把大的问题分解成为若干子问题,只考虑子问题和总问题的关系
  • 想清楚 递归公式递归的终止条件
  • 递归的问题
    • 会出现堆栈溢出的情况
    • 空间复杂度高,函数调用耗时多
    • 重复计算

为了避免重复计算, 我们可以使用散列表来优化斐波那契数列的计算

 // 递归与散列表计算斐波那契数列
 public int f(int n) {
   if (n == 1) return 1;
   if (n == 2) return 2;

   // hasSolvedList可以理解成一个Map,key是n,value是f(n)
   if (hasSolvedList.containsKey(n)) {
     return hasSolvedList.get(n);
   }

   int ret = f(n-1) + f(n-2);
   hasSolvedList.put(n, ret);
   return ret;
 }

链表⭐

链表这个知识点在网络上有很多讲解, 这里仅仅提供一些leetcode刷题记录

L206 反转链表

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var reverseList = function(head) {
    let p = head
    let reverse = null
    while (p!==null) {
        // reverse移动, p移动, node作为临时节点
        const node = p
        p = p.next
        node.next = reverse
        reverse = node
    }
    return reverse
};

L92 指定位置反转链表

上述题目的进阶版, 主要加了一个截断链表的操作

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @param {number} left
 * @param {number} right
 * @return {ListNode}
 */
var reverse = function(link) {
    let p = link
    let reverse = null
    while (p!==null) {
        const node = p
        p = p.next
        node.next = reverse
        reverse = node
    }
    return reverse
}
var reverseBetween = function(head, left, right) {
    let beforeN = null, N=null, M=null, afterM=null
    let dummy = new ListNode(-1, head)
    let cur = dummy
    for (let i=0; i<left-1; i++){
        cur = cur.next
    }
    beforeN = cur
    N = cur.next
    for (let i=0; i<right-left+1; i++){
        cur = cur.next
    }
    M = cur
    afterM = cur.next
    // 截断链表
    beforeN.next = null
    M.next = null
    reverse(N)
    beforeN.next = M
    N.next = afterM
    return dummy.next
};

L160 相交链表

// 暴力解法
function getIntersectionNode (headA, headB) {
  const cacheA = new Set()
  while (headA !== null) {
    cacheA.add(headA)
    headA = headA.next
  }
  while (headB !== null) {
    if (cacheA.has(headB)) return headB
    headB = headB.next
  }
  return null
}

(6 封私信 / 77 条消息) 想见你:教你如何用浪漫的方法找到两个单链表的相交结点 - 知乎 (zhihu.com)

// 优雅解法
function getIntersectionNode (headA, headB) {
    let pA = headA
    let pB = headB
    while (pA!==pB) {
        pA = (pA===null) ? headB : pA.next
        pB = (pB===null) ? headA : pB.next
    }
    return pA
}

栈⭐

  • 栈的本质是FILO, 类似于一个羽毛球筒
  • 也类似于人类解决问题的思路, 即:引入大问题->解决其中的小问题->解决大问题 的思维模式

括号匹配

  • 栈适合处理具有完全包含关系的问题, 而(())正是一种完全包含关系的问题
  • 一对括号代表一组出栈与入栈的操作, 像是( ()() )可以理解为一个大事件解决之前, 要先解决两个小事件, 而大事件一定先发生, 后解决, 这正是FILO的思想
bool isValid(char *s) {
    int32_t lnum = 0, rnum = 0;
    int32_t len = strlen(s);
    for (int32_t i=0; i<len; i++){
        switch (s[i]) {
            case '(' : ++lnum; break;
            case ')' : ++rnum; break;
            defalut : return false;
        }
        if (lnum>=rnum) continue;
        return false;
    }
    return lnum==rnum;
}

中缀转后缀表达式

image.png 数据结构---前缀 中缀 后缀 表达式之间的转换_哔哩哔哩_bilibili

队列

LEETCODE : 20,155,232,844,224,682,496

广义表

【数据结构】两分钟熟练掌握广义表表头表尾计算!_哔哩哔哩_bilibili

二叉树

二叉树中度为1的节点, 要区分左孩子和右孩子; 二叉树是一种递归概念, 它可以表示成如下五种形态

image.png

此外, 二叉树可以分为满二叉树

image.png

image.png

以及完全二叉树

image.png

排序算法⭐

冒泡排序

// 冒泡排序
let arr = [2, 5, 3, 7, 6, 4, 21, 13, 1]
function bubbleSort (nums, flag) {
  let length = nums.length
  for (let i = 0; i < length; i++) {
    let flag = false
    for (let j = 0; j < length - i - 1; j++) {
      if (arr[j] > arr[j + 1]) {
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
        flag = true
      }
    }
    // 如果在内层循环一次后一次交换都没有发生, 直接停止外层循环
    if (!flag) {
      break
    }
  }
  return (flag === 1) ? nums : nums.reverse()
}
console.log(bubbleSort(arr, 1))

插入排序

function insertSort (arr) {
  for (let i = 0; i < arr.length - 1; i++) {
    for (let j = i + 1; j > 0; j--) {
      if (arr[j - 1] > arr[j]) {
        [arr[j - 1], arr[j]] = [arr[j], arr[j - 1]]
      }
    }
  }
  return arr
}
let arr = [2, 5, 3, 7, 6, 4, 21, 13, 1]
console.log(insertSort(arr))

希尔排序

function shellSort (arr) {
  let len = arr.length
  // gap 即为增量
  for (let gap = Math.floor(len / 2); gap > 0; gap = Math.floor(gap / 2)) {
    for (let i = gap; i < len; i++) {
      let j = i
      let current = arr[i]
      while (j - gap >= 0 && current < arr[j - gap]) {
        arr[j] = arr[j - gap]
        j = j - gap
      }
      arr[j] = current
    }
  }
  return arr
}


var arr = [3, 5, 7, 1, 4, 56, 12, 78, 25, 0, 9, 8, 42, 37]
console.log(shellSort(arr))

选择排序

let arr = [2, 5, 3, 7, 6, 4, 21, 13, 1]
function selectSort (arr) {
  let i = 0
  while (i < arr.length - 1) {
    let MinIndex = i
    for (let j = i + 1; j < arr.length; j++) {
      if (arr[j] < arr[MinIndex]) {
        MinIndex = j
      }
    }
    [arr[i], arr[MinIndex]] = [arr[MinIndex], arr[i]]
    i++
  }
  return arr
}
console.log(selectSort(arr))
console.log(selectSort(arr).reverse())

快速排序

快速排序是基于递归和分治的思想实现的

排序流程:

  • 先取出数组中的一个值, 作为基准值
  • 根据这个基准值, 再分为小于基准值的数组和大于基准值的数组
  • 进入递归
  • 当递归的数组小于两个时, 跳出递归, 将这个数组返回基线条件
  • 最后将每个数组拼接在一起

image.png

代码实现:

 const arr = [11, 2, 4, 3, 5, 87]
 function quickSort(arr) {
   if (arr.length<2) {
     return arr
   }
   let goal = arr[0]
   let minArr = arr.slice(1).filter(item=>item<=goal)
   let maxArr = arr.slice(1).filter(item=>item>goal)
   return quickSort(minArr).concat([goal]).concat(quickSort(maxArr))
 }
 console.log(quickSort(arr))

计数排序

算法思想:

  • 找到数组中最大的元素
  • 使用一个数组, 长度等于最大值max+1(数字从0开始),并记录每个元素出现的次数
  • 根据计数数组, 依次输出到新的数组中
function countSort (arr) {
  let countArrayLen = Math.max(...arr) + 1
  let countArray = new Array(countArrayLen).fill(0)
  // 把原数组中的num作为计数数组的索引值, 记录下来
  for (let num of arr) {
    countArray[num]++
  }
  let res = []
  for (let i = 0; i < countArray.length; i++) {
    while (countArray[i] > 0) {
      res.push(i)
      countArray[i]--
    }
  }
  return res
}
let arr = [2, 5, 3, 7, 6, 4, 21, 13, 1]
console.log(countSort(arr))

桶排序

与计数排序的比较:

  • 桶排序是计数排序的拓展版本
  • 计数排序可以看成每个桶只存放一个元素, 桶排序的每个桶需要存放一定范围内的元素
  • 最后都要开辟一个新空间用来存放结果

image.png

function bucketSort (arr) {
  let MaxNum = Math.max(...arr)
  let MinNum = Math.min(...arr)
  // 获取桶的长度
  let bucketLen = Math.floor((MaxNum - MinNum) / arr.length) + 1
  // bucketArr是一个二维数组, 一开始给每个元素填充上一个空数组
  let bucketArr = new Array(bucketLen).fill(null).map(_ => [])
  // 遍历原数组, 将每个元素放入桶数组中
  for (let num of arr) {
    let index = Math.floor((num - MinNum) / arr.length)
    bucketArr[index].push(num)
  }
  const res = []
  for (let item of bucketArr) {
    // bucketArr中的每个元素都是一个数组, 先对它的每一个元素进行排序
    item.sort((a, b) => a - b)
    // 依次放入result数组中
    for (let num of item) {
      res.push(num)
    }
  }
  return res
}
let arr = [2, 5, 3, 7, 6, 4, 21, 13, 1]
console.log(bucketSort(arr))

基数排序

基数排序(Radix Sort)是桶排序的拓展, 它的基本思想是将整数按位数切割成不同的数字, 然后按照每个位进行比较

具体做法是: 将所有待比较数值统一为相同的数位长度, 数位较短的前面补0, 然后, 从最低位开始, 依次进行依次排序, 这样从最低位一直到最高位排序后, 就变成了一个有序数列

image.png

image.png

//LSD Radix Sort  
var arr = [2, 5, 3, 7, 6, 4, 21, 13, 1]
var counter = []
function radixSort (arr, maxDigit) {
  var mod = 10
  var dev = 1
  for (var i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
    for (var j = 0; j < arr.length; j++) {
      var bucket = parseInt((arr[j] % mod) / dev)
      if (counter[bucket] == null) {
        counter[bucket] = []
      }
      counter[bucket].push(arr[j])
    }
    var pos = 0
    for (var j = 0; j < counter.length; j++) {
      var value = null
      if (counter[j] != null) {
        while ((value = counter[j].shift()) != null) {
          arr[pos++] = value
        }
      }
    }
  }
  return arr
}
console.log(radixSort(arr, 2))

归并排序

当遇到一个很大的难题时,我们常常想到的是,能不能把这个事情拆成很多块儿,然后一块儿一块的解决它,最后合起来,这个大难题不就解决了嘛。

也就是我们常说的 ”分而治之“,归并排序可以说是将 分而治之 的思想表现得淋漓尽致。什么意思呢,来看思路,现在有一个数组 arr 需要排序:

  1. 假如说这个数组的左边一半 arr_left 是有序的,右边一半 arr_right 也是有序的,整体不是有序的。那么将 arr_left 和 arr_right 做一次归并操作后,是不是最终就能得到一个有序的数组呀!
  2. 又假如,数组的左边一半 arr_left 它整体是无序的,但是它的左边一半 arr_left_left 是有序的,并且它的右边一半 arr_left_right 是有序的。哎,那将 arr_left_left 和 arr_left_right 做一次归并操作后,是不是就能得到一个有序的数组呀,也就是 arr_left 就有序了呀
  3. 是不是就是一个拆分的过程呀,将大数组拆分成小数组,然后一直假设下去...
  4. 直到什么时候呢,直到这个小数组只有一个元素了,只有一个元素了是不是就代表着这个数组有序了呀,然后再一层一层的做归并操作,每做一次归并操作,两个小数组就能合成一个大的有序数组
  5. 最终,这个大数组是不是就能有序了呀

总结:”分而治之“,要将一个数组排序,可以先(递归地)将数组分成两半排序,然后再依次归并起来。

整体的思路如下:

image.png

// 合并两个有序数组(在两个数组上归并)
function merge_two_sortedArr (arr1, arr2) {
  const l1 = arr1.length
  const l2 = arr2.length
  let p = 0
  let q = 0
  let res = []
  while (p < l1 || q < l2) {
    if (p === l1) {
      res.push(arr2[q++])
    } else if (q === l2) {
      res.push(arr1[p++])
    } else if (arr1[p] < arr2[q]) {
      res.push(arr1[p++])
    } else {
      res.push(arr2[q++])
    }
  }
  return res
}
let arr1 = [1, 4, 6]
let arr2 = [3, 12, 88]
console.log(merge_two_sortedArr(arr1, arr2))
// 在原数组上进行归并
function merge_in_place (arr, l, mid, r) {
  let keep = []
  for (let i = l; i <= r; i++) {
    keep[i] = arr[i]
  }
  let p = l
  let q = mid + 1
  for (let i = l; p <= mid || q <= r; i++) {
    if (p === mid + 1) {
      arr[i] = keep[q++]
    } else if (q === r + 1) {
      arr[i] = keep[p++]
    } else if (keep[p] < keep[q]) {
      arr[i] = keep[p++]
    } else {
      arr[i] = keep[q++]
    }
  }
  return keep
}
let arr = [1, 4, 5, 2, 3, 6, 7]
merge_in_place(arr, 0, 2, arr.length - 1)
console.log(arr)
// 归并排序
function merge (arr, l, mid, r) {
  const keep = []
  for (let i = l; i <= r; i++) {
    keep[i] = arr[i]
  }

  for (let p = l, q = mid + 1, k = l; p <= mid || q <= r;) {
    if (p === mid + 1) arr[k++] = keep[q++]
    else if (q === r + 1) arr[k++] = keep[p++]
    else if (keep[p] > keep[q]) arr[k++] = keep[q++]
    else arr[k++] = keep[p++]
  }
}

function mSort (arr, l, r) {
  if (l >= r) return
  // 分半
  const mid = l + ((r - l) >> 1)
  // 归并排序左侧
  mSort(arr, l, mid)
  // 归并排序右侧
  mSort(arr, mid + 1, r)
  // 走到这里, l~mid 部分已排序,mid+1~r 部分已排序
  // 直接原地归并操作就可以实现数组排序
  merge(arr, l, mid, r)
}

function MergeSort (arr) {
  mSort(arr, 0, arr.length - 1)
}

const nums = [12, 1, 7, 4, 5, 2, 10, 6, 3, 11, 9, 8, 13]
MergeSort(nums)
console.log(nums);

动态规划

保证最优解

贪心

快速找到比较优的解

Javascript源码

  • Javascript里的sort是什么排序?
    • v8里当数组长度小于10用插入排序, 否则使用快速排序
    • 现版本采用Timsort排序, 它源于归并排序和插入排序, 是一种混合与稳定的排序算法