数据结构

数组

  • 有下标
  • 顺序存储

数组的基本操作

1、读取元素

通过下标读取

2、更新元素

通过下标赋值

3、插入元素

  • 尾部插入
  • 中间插入
  • 超范围插入

中间插入需将插入位置后面的元素往后移动,把要插入的元素放到插入位置。

function insert(array, element, index) {
  const size = array.length;
  if (index < 0 || index > size) {
    console.error('超出数组实际元素范围');
  }
  // 从右向左循环,将元素逐个移动
  for (let i = size - 1; i >= index; i--) {
    array[i + 1] = array[i];
  }
  array[index] = element;
  return array;
}

console.log(insert([1, 2, 3, 4, 5], 1, 2)); // [1, 2, 1, 3, 4, 5]

超范围插入,需要将数组扩容--将数组扩充到原来的2倍,然后将原来的元素复制过去。

4、删除元素

如果删除元素位于数组中间,需要将后面的元素向前移动。

function del(array, index) {
  const size = array.length;
  if (index < 0 || index > size) {
    console.error('超出数组实际元素范围');
  }
  for (let i = index; i < size; i++) {
    array[i] = array[i + 1];
  }
  array.length--;
  return array;
}
console.log(del([1, 2, 3, 4, 5], 2)); // [1, 2, 4, 5]

数组的优势劣势

优势:高效的随机访问能力

劣势:插入、删除元素会导致大量元素移动位置,影响效率。

链表

链表(linked list)是一种在物理上非连续、非顺序的数据结构,由若干节点所组成。

单向链表

单向链表的每个节点有两个部分组成:1、存放数据的变量data;2、指向下一个节点的指针 next。

img

双向链表

双向链表除了有 data 和 next 指针外,还有指向前一个节点的 prev 指针。

img

链表的基本操作

1、查找节点

查找第三个节点,需要从头节点开始,查找第二个节点,再查找第三个节点。时间复杂度最长为 O(n)。

2、更新节点

不考虑查找节点的过程,链表的更新过程和数组一样简单,直接替换成新数据即可。

3、插入节点

  • 尾部插入
  • 头部插入
  • 中间插入

尾部插入:将最后一个节点的 next 指针指向新插入的节点即可。

头部插入:将新节点的 next 指针指向原来的头节点,把新节点变为链表的头节点。

中间插入:新节点的 next 指针指向插入位置的节点,插入位置的前一个节点的 next 指针指向新节点。

4、删除元素

  • 头部删除
  • 中间删除
  • 尾部删除

尾部删除:将倒数第二个节点的 next 指向空。

头部删除:把原来的头节点的next指针指向的节点设为头节点。

中间删除:把删除位置的前一个节点的next指向删除位置的下一个节点。

数组 vs 链表

数组优势在于能够快速定位元素,对于读操作多,写操作少的场景,数组更适合。

链表相反。

栈和队列

物理结构和逻辑结构

物理结构:内存中实实在在的存储结构。

包括顺序存储结构(数组)、链式存储结构(链表)。

逻辑结构:抽象概念,依赖物理结构存在。

包括线性(顺序表、栈、队列)、非线性(树、图)。

特点:先进后出,后进先出。

最先进的元素位于栈底,最后进的元素位于栈顶。

基本操作:

  • 入栈:将新元素放入栈中,不过只能从栈顶放入,新元素的位置成为栈顶。
  • 出栈:将元素弹出栈,只能从栈顶弹出,弹出元素的前一个元素的位置成为栈顶。

img

img

因为出栈入栈都只影响到最后一个元素,时间复杂度:O(1)。

队列

特点:先进先出,后进后出。

队列的出口端叫队头,队列的入口端叫队尾。

基本操作:

  • 入队
  • 出队

链表的入队、出队方式和栈基本相同,但是数组不一样。

用数组实现时,为了操作方便,把最后一个元素的下一个位置定位队尾。

img

数组的入队:只能从队尾加入,新元素的下一个位置变成队尾。

img

数组的出队:只能在队头一侧移除元素,出队元素的后一个元素成为新的队头。

img

如果不断出队,队列的容量会变得越来越小,需要采用循环队列的方式来维持队列的容量。

循环队列保留了出队元素的位置,新元素进来时,将队尾指向数组的首位,再进来新元素时,队尾往后一位移动,以此类推,直到 队尾下标+1 % 数组长度 === 0。

循环队列利用了数组的空间,避免了元素的移动。

入队出队的时间复杂度:O(1)

双端队列

既可以先进先出,也可以先进后出。

优先队列

优先级高的先出队。

不属于线性数据结构,基于二叉堆实现的。

散列表

也叫哈希表(hash table)。

提供 key: value 的映射,只要给出一个 key 就能高效找到所匹配的 value。时间复杂度:O(1)

哈希函数

散列表本质上也是一个数组。

通过某种方式,把 key 和数组下标进行转换——哈希函数。

散列表的读写操作

1、写操作

在散列表中插入新的键值对(key-value)。

1)通过哈希函数把 key 转换成数组的下标,比如 5
2) 如果数组下标 5 对应的位置没有元素,就把 value 写入到数组下标 5 的位置。

因为数组长度是有限的,插入的键值对越来越多,不同的 key 通过哈希函数获得的下标有可能是相同的。这种情况叫__哈希冲突__。

哈希冲突解决方法:

  • 开放寻址法
  • 链表法

开放寻址法:当一个 key 通过哈希函数获得对应的数组下标已被占用时,可以寻找下一个空档的位置。

链表法:

HashMap 数组的每个元素不仅时一个键值对,还是一个链表的头节点。每个键值对的 next 指向下一个键值对。

当新来的键值对映射到与之冲突的数组位置时,只需要插入到对应的链表中。

2、读操作

通过给定的 key,在散列表中查找对应的 value。

  1. 通过哈希函数把 key 转换成数组的下标,比如 5
  2. 找到下标为 5 的数组元素,如果对应的 key 是对的,则返回 value,否则继续在链表中查找。

3、扩容

散列表是基于数组的,所以也涉及扩容的问题。

当大量元素拥挤在相同数组下标下,形成很长的链表时,对后续插入和查找性能有很大影响时,需要扩容。

影响扩容的因素:

  • Capacity: HashMap 的当前长度
  • LoadFactor: HashMap 的负载因子,默认值为 0.75f

衡量是否需要扩容:

HashMap.size >= Capacity * LoadFactor

扩容操作:

1)扩容,创建一个新的 Entry 空数组,长度是原数组的 2 倍。
2)重新 Hash,遍历原 Entry 数组,把所有的 Entry 数组重新 Hash 到新数组中。-- 数组长度改变,Hash 规则随之改变。

Last Updated: 8/23/2019, 2:51:38 PM