常用的数据结构(一)

171 阅读4分钟

对于前端开发我们需要知道常用的数据结构,这样在开发的过程中才能写出高质量的代码。

所以我对于前端常用的或者需要知道的数据结构进行记录并且根据leetcode题进行运用。

前端常用的数据结构有以下几个:

1. 数组、字符串 / Array & String
2. 链表 / Linked-list
3. 栈 / Stack
4. 单调栈 / Monotonous-Stack
4. 队列 / Queue
5. 双端队列 / Deque
6. 树 / Tree

数组

优点:

  1. 可以容纳多个值:数组是一种容器,可以存储多个值。它们可以很方便地用来组织数据。

  2. 快速访问和修改:由于数组的索引是数字,因此可以快速访问和修改特定位置的值。

  3. 可以存储不同类型的值:JavaScript数组可以存储任何类型的值,包括数字、字符串、对象等等

缺点:

  1. 数组的长度是固定的:一旦数组创建,其长度就是固定的。如果需要添加或删除元素,可能需要创建一个新的数组,这可能会导致性能问题。

  2. 数组是线性的:由于数组的数据是连续存储的,因此插入或删除元素可能需要大量移动其他元素,这可能会导致性能问题。

  3. 数组的内存管理:由于数组是连续的存储空间,因此在创建数组时需要一次性分配所有内存。如果数组很大,可能会占用过多的内存,从而导致性能问题。

数组最关心的内存问题:

在内存方面,JavaScript中的数组是一段连续的内存空间,可以存储多个值。当数组创建时,JavaScript会为其分配一段连续的内存空间,以存储该数组的元素。

每个数组元素在内存中都有一个固定的地址,通过数组索引可以快速访问该地址。例如,对于一个长度为5的数组,它的第一个元素的地址是数组的基地址,第二个元素的地址是基地址加上每个元素占用的内存空间,以此类推。

JavaScript中的数组在内存管理方面也有一些限制。由于数组是连续存储的,因此在创建数组时需要一次性分配所有内存。如果数组很大,可能会占用过多的内存,从而导致性能问题。

另外,由于JavaScript是动态类型语言,数组中可以存储任何类型的值,包括数字、字符串、对象等等。这意味着在创建数组时,需要预留足够的内存空间以存储不同类型的值,而且这些值的大小和内存占用可能不同,这也可能导致内存浪费。

在 JavaScript 中,如果数组中的某个元素被清空或者从数组中删除,那么其所占用的内存空间将被释放,以便可以再次使用。然而,如果数组长度不会改变,那么原始分配的内存空间将不会自动释放,这可能会导致内存泄漏问题。

leetcode 283. 移动零

// 思路就是双指针 right只要是非零的数就交换,
var moveZeroes = function(nums) {
    let left = 0;
    let right = 0;
    while(right<nums.length) {
        if (nums[right] != 0) {
            [nums[left], nums[right]] = [nums[right], nums[left]]
            left++
        }
        right++
    }

    return nums
};

字符串

优点:

  1. 可以容纳任何字符:JavaScript中的字符串可以包含任何Unicode字符,包括表情符号、特殊字符等等。

  2. 可以轻松地进行文本处理:JavaScript提供了许多内置字符串方法,可以轻松地进行字符串的搜索、替换、截取、拼接等处理。

  3. 可以作为对象属性名:在JavaScript中,字符串可以作为对象的属性名,这为对象属性的访问和操作提供了很大的灵活性。

  4. 不可变性:JavaScript中的字符串是不可变的,也就是说,一旦创建了一个字符串,就不能对其进行修改。这样可以保证字符串的安全性和稳定性。

缺点:

  1. 内存占用:由于JavaScript中的字符串是不可变的,因此每次对字符串进行修改时,都需要创建一个新的字符串对象,这可能会导致内存占用问题。

  2. 无法直接操作字符:JavaScript中的字符串是基于字符序列的,因此无法直接访问或修改其中的某个字符。如果需要对字符串中的某个字符进行操作,需要先将字符串转换为数组。

leetcode 242. 有效的字母异位词

// 首先第一种解法sort排序比较大小
sort默认排序顺序是在将元素转换为字符串,然后比较它们的 UTF-16 代码单元值序列时构建的
charCodeAt查看a-z的索引处的 UTF-16 代码单元是一个97-122之间的整数
'a'.charCodeAt() === 97
'z'.charCodeAt() === 122
sort默认是根据这个代码单元的整数进行排序的

// 所以第一种解法我们很容易得出
var isAnagram = function(s, t) {
    if (s.length !== t.length) {
        return false
    }
    s = s.split('').sort().join('')
    t = t.split('').sort().join('')
    return s === t
};

// 第二种解法 map
我们可以在想到一种方式就是把字符串插入到map中
我们循环s和t s插入 t删除

// 由此第二种解法我们也很容易得出
var isAnagram = function(s, t) {
    if (s.length !== t.length) {
        return false
    }
    var temp = {}
    for(let i=0; i<s.length; i++) {
        if (temp[s[i]]) {
            temp[s[i]]++
        } else {
            temp[s[i]] = 1
        }
    }
    for(let i=0; i<t.length; i++) {
        if (temp[t[i]]) {
            temp[t[i]]--
        } else {
            return false
        }
    }
    return true
};

链表

在JavaScript中,链表是一种动态数据结构,由一系列节点组成,每个节点都包含一个值和一个指向下一个节点的指针。

 function ListNode(val, next) {
     this.val = (val===undefined ? 0 : val)
     this.next = (next===undefined ? null : next)
 }

优点:

  1. 灵活性:链表的长度可以动态改变,可以插入和删除节点。

  2. 内存利用率高:相比于数组,链表在插入和删除节点时不需要移动其他节点,因此可以更高效地利用内存。

  3. 可以节约空间:链表可以动态分配内存,因此可以节约空间,避免浪费。

  4. 方便实现其他数据结构:链表可以作为其他数据结构的基础,例如栈、队列和哈希表等。

缺点:

  1. 访问和搜索元素需要遍历整个链表,效率较低。

  2. 链表不支持随机访问,只能顺序访问。

  3. 链表的节点指针需要额外的内存空间,会占用一定的空间。

leetcode 25. K 个一组翻转链表

这个题的思路很简单

1、用k来分割子链表

2、反转子链表

3、连接子链表

在链表的数据结构中,假的头节点很重要,可以方便我们很多。

主要就是三个指针 pre head

// 反转子链表、增加了一个子链表的结束节点
const reverse = (head, tail) => {
    let prev = null;
    let cur = head;
    let temp=null;
    // 判断反转的子链表结束的位置,反转后tail为头结点
    while (prev !== tail) {
        temp = p.next;
        cur.next = prev;
        prev = p;
        cur = temp;
    }
    // 修改的是next的引用地址
    return [tail, head];
}
var reverseKGroup = function(head, k) {
    // 创建一个头结点。返回值是hair.next
    const hair = new ListNode(0);
    hair.next = head;
    let pre = hair;
    
    while (head) {
        let tail = pre;
        // 查看剩余部分长度是否大于等于 k
        for (let i = 0; i < k; ++i) {
            tail = tail.next;
            if (!tail) {
                return hair.next;
            }
        }
        // 保存当前子链表尾节点的下一个节点,反转链表后连接用
        const nex = tail.next;
        [head, tail] = reverse(head, tail);
        // 把子链表重新接回原链表
        pre.next = head;
        tail.next = nex;
        // 进入下一个需要处理的子链表
        // pre 还是代表子链表的头部前的节点
        pre = tail;
        // 子链表的开始节点
        head = tail.next;
    }
    return hair.next;
};

在JavaScript中,栈是一种简单的数据结构,它的主要特点是“后进先出”(LIFO)。JavaScript中的栈常常用于实现递归算法、表达式求值和函数调用等场景。

优点:

  1. 简单易用:栈的操作非常简单,只需要掌握入栈和出栈两个操作即可。

  2. 内存利用率高:相比于其他数据结构,栈的内存利用率较高,因为它只需要存储元素本身和指向前一个元素的指针。

  3. 高效性:栈的操作时间复杂度为O(1),非常高效。

  4. 支持递归操作:栈可以轻松地实现递归算法,例如深度优先搜索。

缺点:

  1. 无法随机访问:栈只能按照后进先出的顺序进行访问,不支持随机访问。

  2. 存储空间有限:栈的大小是固定的,存储空间有限。

  3. 不适合存储大量数据:栈不适合存储大量数据,因为其容易导致栈溢出。

leetcode 20. 有效的括号

这个题的思路很简单

思路就是用map存储口括号的闭合

然后不是闭合的都存进栈中

每次不在map中的就比较栈顶的和循环的是不是一对括号

var isValid = function(s) {
    // 奇数直接返回
    if (s % 2 == 1) {
        return false
    }

    let map = {
        '{': '}',
        '(': ')',
        '[': ']'
    }
    let stack = []
    for(let i = 0; i < s.length ; i++) {
        if(map[s[i]]) {
            stack.push(s[i])
        } else if(s[i] !== map[stack.pop()]){
            return false
        }
    }
    return stack.length === 0
};

单调栈

单调栈(monotonic stack)是一种特殊的栈,它的元素具有单调性。具体来说,单调栈可以分为单调递增栈和单调递减栈。

单调递增栈是指栈中元素的值单调递增,即栈底元素是最小的,栈顶元素是最大的。

单调递减栈是指栈中元素的值单调递减,即栈底元素是最大的,栈顶元素是最小的。

单调栈的主要应用是解决一些关于数组的单调性问题。例如,求解下一个更大元素、下一个更小元素、最大矩形面积等问题。其基本思想是利用栈的单调性,不断弹出栈顶元素,直到满足某种条件为止。

单调栈具有以下特点:

  1. 栈中元素具有单调性,可以利用这个特点进行快速查找。

  2. 可以通过维护一个单调栈,实现对数组中元素的快速处理。

  3. 单调栈的时间复杂度为O(n),非常高效。

leetcode 739. 每日温度

// 单调栈
// 方法1:双指针
var dailyTemperatures = function(temperatures) {
    let result = [];
    let left = 0;
    let right = 0;
    
    while(left < temperatures.length) {
        if (right == temperatures.length) {
            result.push(0)
            left++;
            right = left+1
        } else {
        if (temperatures[left] < temperatures[right]) {
            result.push(right-left)
            left++;
            right = left+1
        } else {
            right++
        }
        }
    }

    return result
};
// 方法2:单调栈 维持一个栈数字的顺序
var dailyTemperatures = function(temperatures) {
    // 定义单调栈 存储索引和value
    const lengh = temperatures.length
    let result = new Array(lengh).fill(0)
    let index=0;
    let stack = [];

    while(index<lengh) {
        while(stack.length && temperatures[index]>stack[stack.length-1]['value']) {
            let temp = stack.pop()
            result[temp['index']] = index - temp['index']
        }
        stack.push({ index: index, value: temperatures[index] })
        index++
    }
    return result
};

队列

在JavaScript中,队列是一种简单的数据结构,它的主要特点是“先进先出”(FIFO)。JavaScript中的队列通常用于实现广度优先搜索、任务队列和消息队列等场景。

优点:

  1. 简单易用:队列的操作非常简单,只需要掌握入队和出队两个操作即可。

  2. 内存利用率高:相比于其他数据结构,队列的内存利用率较高,因为它只需要存储元素本身和指向前一个元素的指针。

  3. 高效性:队列的操作时间复杂度为O(1),非常高效。

  4. 支持多线程操作:队列可以支持多线程操作,例如消息队列等。

缺点:

  1. 无法随机访问:队列只能按照先进先出的顺序进行访问,不支持随机访问。

  2. 存储空间有限:队列的大小是固定的,存储空间有限。

  3. 不适合存储大量数据:队列不适合存储大量数据,因为其容易导致内存溢出。

leetcode 239. 滑动窗口最大值

// 双端队列中,满足单调性的双端队列一般称作「单调队列」
var maxSlidingWindow = function (nums, k) {
  let result = []
  let queue = []
  let i = 0
  while (i < k) {
    while (queue.length && nums[i] > nums[queue[queue.length - 1]]) {
      queue.pop()
    }
    queue.push(i)
    i++
  }
  result.push(nums[queue[0]])
  for (let i = k; i < nums.length; i++) {
    while (queue.length && nums[i] > nums[queue[queue.length - 1]]) {
      queue.pop()
    }
    queue.push(i)
    while (queue[0] <= i - k) {
      queue.shift()
    }
    result.push(nums[queue[0]])
  }
  return result
}

无重复字符的最长子串

双端队列

双端队列(deque,即double-ended queue)是一种特殊的队列,可以从两端添加和删除元素。它可以看作是栈和队列的结合体,因为它既支持队列的先进先出特性,又支持栈的后进先出特性。双端队列可以从队列的任一端进行插入或删除操作,因此它是一种非常灵活的数据结构。

双端队列的主要特点包括:

  1. 可以从队列的两端进行插入或删除操作,因此它是一种非常灵活的数据结构。

  2. 可以在常数时间内执行队列头部或尾部的插入和删除操作。

  3. 可以用于解决一些需要同时从队列头部和尾部进行操作的问题,例如维护滑动窗口的最大值、最小值等。

需要注意的是,双端队列的实现可以采用数组或链表等数据结构。采用数组实现的双端队列需要在插入和删除时移动元素,因此插入和删除的时间复杂度为O(n)。而采用链表实现的双端队列可以在常数时间内进行插入和删除操作,因此更加高效。

// 双端队列解决
var lengthOfLongestSubstring = function(s) {
    let rightIndex = 0;
    let res = 0
    let n = s.length;
    let obj = new Set();
    for (let i=0; i<n; ++i) {
        if (i != 0) {
            obj.delete(s.charAt(i-1))
        }

        while(rightIndex < n && !obj.has(s.charAt(rightIndex))) {
            obj.add(s.charAt(rightIndex));
            ++rightIndex;
        }

        res = Math.max(res, rightIndex - i)
    }
    return res
};

在JavaScript中,树是一种非常常用的数据结构,它由一个或多个节点组成,节点之间通过边连接。

优点:

  1. 能够快速地查找、插入和删除元素。

  2. 能够进行高效的排序和搜索操作,例如二叉搜索树(BST)。

  3. 能够进行快速的合并和分割操作,例如堆。

缺点:

  1. 树的结构比较复杂,实现起来相对困难。

  2. 树的平衡性需要得到保证,否则可能会导致树的深度过大,影响性能。

  3. 树的节点数量不易控制,如果节点数量过多,可能会占用大量内存。

需要根据具体的使用场景来选择树这种数据结构,对于一些需要进行高效的查找、排序、合并和分割操作的问题,树是一种非常合适的数据结构。

230. 二叉搜索树中第K小的元素

// 二叉搜索树的特点就是中序遍历是递增的

var kthSmallest = function (root, k) {
  let result
  let fun = function (node) {
    if (node !== null && k > 0) {
      fun(node.left)
      if (--k === 0) {
        result = node.val
        return
      }
      fun(node.right)
    }
  }
  fun(root)
  return result
}