初探数据结构

225 阅读4分钟

1、数据结构

  • 数组
    • 数组可以分为队列、栈
  • 哈希表
    • 用来存储 key-value 对

2、队列Queue

  • 特点: 先进先出 的数组
  • 进入:push(),出:shift()
  • 代码:(吃饭叫号)
  • 选择queue作为数据结构
  • queue.push 为入队,queue.shift 为出队

题目: 请实现一个餐厅叫号网页

  • 点击「取号」按钮生成一个号码
  • 点击「叫号」按钮显示请X号就餐

手写队列Queue

  • 首先选择队列Queue作为数据结构
  • queue.push()为入队,queue.shift()为出队
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <link rel="stylesheet" href="./style.css" />
  </head>
  <style>
    #screen {
        border: 1px solid black;
        width: 200px;
        height: 200px;
    }
  </style>
  <body>
    <div id="screen"></div>
    <div class="actions">
      <button id="createNumber">取号</button>
      <button id="callNumber">叫号</button>
    </div>
    <div>当前号码 <span id="newNumber"></span></div>
    <div>当前队列 <span id="queue"></span></div>
    <script src="./main.js"></script>
  </body>
</html>
const divScreen = document.querySelector("#screen");
const btnCreateNumber = document.querySelector("#createNumber");
const btnCallNumber = document.querySelector("#callNumber");
const spanNewNumber = document.querySelector("#newNumber");
const spanQueue = document.querySelector("#queue");

let n = 0;
let queue = [];

btnCreateNumber.onclick = () => {
  n += 1;
  queue.push.call(queue, n);    //queue.push(n)
  spanNewNumber.innerHTML = n;
  spanQueue.innerHTML = JSON.stringify(queue);
};

btnCallNumber.onclick = () => {
  if (queue.length === 0) {
    divScreen.innerText = "没有客人了";
    return;
  }
  const m = queue.shift.call(queue);   //const m = queue.shift()
  divScreen.innerHTML = `请 ${m} 号就餐`;
  spanQueue.innerHTML = JSON.stringify(queue);
};

3、栈Stack

  • 特点:后进先出 的数组
  • 压栈:push(),弹栈:pop()
  • JS函数的调用栈 call stack 就是一个栈
    • 假设 f1 调用了 f2,f2 又调用了 f3
    • 那么 f3 结束后应该回到 f2,f2 结束后应该回到 f1

4、链表 Linked List

const createList = (value) => {
  return {
    data: value,
    next: null,
  };
};
const appendList = (list, value) => {
  const node = {
    data: value,
    next: null,
  };
  let x = list;
  while (x.next) {
    x = x.next;
  }
  //x.next ===null //x是最后一个节点
  x.next = node;
  return node;
};

const removeFromList = (list, node) => {
  let x = list;
  let p = null;
  while (x !== node) {
    p = x;
    x = x.next;
  }
  p.next = x.next;
};

<!-- 删除节点的整体思路 -->
<!--const removeFromList = (list, node)=>{-->
<!--    // 遍历-->
<!--    if(list === node){-->
<!--    // 如果删除的是第1个节点-->
<!--        list = node.next-->
<!--        // 第一个节点 自动被回收-->
<!--    }else{-->
<!--    // 如果删除的是第2个节点-->
<!--    // 就让第1个节点.next = 第2个节点.next-->
<!--        if(list.next === node){-->
<!--            list.next = node.next-->
<!--            // 第2个节点 自动被回收-->
<!--        }else{-->
<!--            // 开始递归了-->
<!--    // 如果删除的是第3个节点-->
<!--    // 就让第2个节点.next = 第3个节点.next-->
<!--            if(list.next.next === node){-->
<!--                (list.next).next = node.next-->
<!--            }-->
<!--        }-->
<!--    }-->
<!--}-->


const travelList = (list, fn) => {
  let x = list;
  while (x !== null) {
    fn(x);
    x = x.next;
  }
};

const list = createList(10);
const node2 = appendList(list, 20);
console.log(node2);
const node3 = appendList(list, 30);
const node4 = appendList(list, 40);
const node5 = appendList(list, 50);
travelList(list, (node) => {
  console.log(node.data);
});
const node = list;
removeFromList(list, node3);

console.log("list:");
console.log(list);

输出:

其中 removeFromList 有一个问题,它无法删除第一个节点

const list = createList(10);
const node = list // node 就是 list 的第一个节点
removeFromList(list, node) // list 没有任何变化
const newList = removeFromList(list, node) // 就算获取返回值也没有用,因为根本就没返回新 list

正确的实现方法为:

const removeFromList = (list, node) => {
  let x = list;
  let p = node; // 课堂里将 p 初始化为 null,这里改为 node
  while (x !== node && x !== null) { // 课堂里忘了对 null 进行处理,如果 node 不在 list 中,x 就可能为 null
    p = x;
    x = x.next;
  }
  if(x === null){ // 若 x 为 null,则不需要删除,直接 returnfalse 表示无法删除不在list里的节点
    return false
  }else if(x === p){ // 这说明要删除的节点是第一个节点
    p = x.next
    return p // 如果删除的是第一个节点,那么就要把新 list 的头节点 p 返回给外面,即 newList = removeFromList(list, list)
  }else{
    p.next = x.next;
    return list // 如果删除的不是第一个节点,返回原来的 list 即可
  }
};


const list = createList(10);
const node = list   // node 就是 list 的第一个节点了现在
const newList = removeFromList(list, node) // 必须用 newList 获取返回值才能拿到删除了第一个节点的新 list
  • 链表
    • 双向链表:每个节点有一个 previous 指向上一个节点
    • 循环链表:最后一个节点的 next 指向头节点

5、哈希表 hashTable

  • 场景
    • 假设哈希表 hash 里有一万对 key-value
    • 比如 name: 'wbh' , age: 18 ,p1: 'property1' .....
    • 如何使得读取 hash['xxx'] 速度最快
  • 解决办法
    • 不做任何优化,hash['xxx'] 需要遍历所有 key , 算数复杂度为 O(n)
    • 对 key 排序,使用二分查找, 复杂度为 O(log2n)
    • 用字符串对应的 ASCII 数字做索引, 对索引做除法取余数,O(1),冲突了怎么办,冲突了就顺延

6、树Tree(一个链多个)

  • 实际使用
    • 中国的省市区,可以看成一棵树
    • 公司的层级结构,可以看成一棵树
    • 网页中的节点,可以看成一棵树
let createTree = (value) => {
  return {
    data: value,
    children: null,
    parent: null,
  };
};

const addChild = (node, value) => {
  const newNode = {
    data: value,
    children: null,
    parent: node,
  };
  node.children = node.children || []; //node.children 不为空,则等于自身,为空就是一个空数组[]
  node.children.push(newNode);
  return newNode;
};

const travel = (tree, fn) => {
  fn(tree);
  if (!tree.children) {
    //如果children为空,直接返回
    return false;
  }
  for (let i = 0; i < tree.children.length; i++) {
    travel(tree.children[i], fn);
  }
};

const removeNode = (tree, node) => {
  const siblings = node.parent.children;
  let index = 0;
  for (let i = 1; i < siblings.length; i++) {
    if (node === siblings[i]) {
      index = i;
    }
  }
  siblings.splice(index, 1);
};

const tree = createTree(10);
const node2 = addChild(tree, 20);
const node3 = addChild(tree, 30);
const node4 = addChild(tree, 40);

addChild(tree, 50);
const node21 = addChild(node2, 201);
const node22 = addChild(node2, 202);
addChild(node2, 203);
addChild(node2, 204);

travel(tree, (node) => {
  console.log(node.data);
});

removeNode(tree, node22);
console.log(tree);

输出: