对于前端开发我们需要知道常用的数据结构,这样在开发的过程中才能写出高质量的代码。
所以我对于前端常用的或者需要知道的数据结构进行记录并且根据leetcode题进行运用。
前端常用的数据结构有以下几个:
1. 数组、字符串 / Array & String
2. 链表 / Linked-list
3. 栈 / Stack
4. 单调栈 / Monotonous-Stack
4. 队列 / Queue
5. 双端队列 / Deque
6. 树 / Tree
数组
优点:
-
可以容纳多个值:数组是一种容器,可以存储多个值。它们可以很方便地用来组织数据。
-
快速访问和修改:由于数组的索引是数字,因此可以快速访问和修改特定位置的值。
-
可以存储不同类型的值:JavaScript数组可以存储任何类型的值,包括数字、字符串、对象等等
缺点:
-
数组的长度是固定的:一旦数组创建,其长度就是固定的。如果需要添加或删除元素,可能需要创建一个新的数组,这可能会导致性能问题。
-
数组是线性的:由于数组的数据是连续存储的,因此插入或删除元素可能需要大量移动其他元素,这可能会导致性能问题。
-
数组的内存管理:由于数组是连续的存储空间,因此在创建数组时需要一次性分配所有内存。如果数组很大,可能会占用过多的内存,从而导致性能问题。
数组最关心的内存问题:
在内存方面,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
};
字符串
优点:
-
可以容纳任何字符:JavaScript中的字符串可以包含任何Unicode字符,包括表情符号、特殊字符等等。
-
可以轻松地进行文本处理:JavaScript提供了许多内置字符串方法,可以轻松地进行字符串的搜索、替换、截取、拼接等处理。
-
可以作为对象属性名:在JavaScript中,字符串可以作为对象的属性名,这为对象属性的访问和操作提供了很大的灵活性。
-
不可变性:JavaScript中的字符串是不可变的,也就是说,一旦创建了一个字符串,就不能对其进行修改。这样可以保证字符串的安全性和稳定性。
缺点:
-
内存占用:由于JavaScript中的字符串是不可变的,因此每次对字符串进行修改时,都需要创建一个新的字符串对象,这可能会导致内存占用问题。
-
无法直接操作字符: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)
}
优点:
-
灵活性:链表的长度可以动态改变,可以插入和删除节点。
-
内存利用率高:相比于数组,链表在插入和删除节点时不需要移动其他节点,因此可以更高效地利用内存。
-
可以节约空间:链表可以动态分配内存,因此可以节约空间,避免浪费。
-
方便实现其他数据结构:链表可以作为其他数据结构的基础,例如栈、队列和哈希表等。
缺点:
-
访问和搜索元素需要遍历整个链表,效率较低。
-
链表不支持随机访问,只能顺序访问。
-
链表的节点指针需要额外的内存空间,会占用一定的空间。
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中的栈常常用于实现递归算法、表达式求值和函数调用等场景。
优点:
-
简单易用:栈的操作非常简单,只需要掌握入栈和出栈两个操作即可。
-
内存利用率高:相比于其他数据结构,栈的内存利用率较高,因为它只需要存储元素本身和指向前一个元素的指针。
-
高效性:栈的操作时间复杂度为O(1),非常高效。
-
支持递归操作:栈可以轻松地实现递归算法,例如深度优先搜索。
缺点:
-
无法随机访问:栈只能按照后进先出的顺序进行访问,不支持随机访问。
-
存储空间有限:栈的大小是固定的,存储空间有限。
-
不适合存储大量数据:栈不适合存储大量数据,因为其容易导致栈溢出。
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)是一种特殊的栈,它的元素具有单调性。具体来说,单调栈可以分为单调递增栈和单调递减栈。
单调递增栈是指栈中元素的值单调递增,即栈底元素是最小的,栈顶元素是最大的。
单调递减栈是指栈中元素的值单调递减,即栈底元素是最大的,栈顶元素是最小的。
单调栈的主要应用是解决一些关于数组的单调性问题。例如,求解下一个更大元素、下一个更小元素、最大矩形面积等问题。其基本思想是利用栈的单调性,不断弹出栈顶元素,直到满足某种条件为止。
单调栈具有以下特点:
-
栈中元素具有单调性,可以利用这个特点进行快速查找。
-
可以通过维护一个单调栈,实现对数组中元素的快速处理。
-
单调栈的时间复杂度为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中的队列通常用于实现广度优先搜索、任务队列和消息队列等场景。
优点:
-
简单易用:队列的操作非常简单,只需要掌握入队和出队两个操作即可。
-
内存利用率高:相比于其他数据结构,队列的内存利用率较高,因为它只需要存储元素本身和指向前一个元素的指针。
-
高效性:队列的操作时间复杂度为O(1),非常高效。
-
支持多线程操作:队列可以支持多线程操作,例如消息队列等。
缺点:
-
无法随机访问:队列只能按照先进先出的顺序进行访问,不支持随机访问。
-
存储空间有限:队列的大小是固定的,存储空间有限。
-
不适合存储大量数据:队列不适合存储大量数据,因为其容易导致内存溢出。
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)是一种特殊的队列,可以从两端添加和删除元素。它可以看作是栈和队列的结合体,因为它既支持队列的先进先出特性,又支持栈的后进先出特性。双端队列可以从队列的任一端进行插入或删除操作,因此它是一种非常灵活的数据结构。
双端队列的主要特点包括:
-
可以从队列的两端进行插入或删除操作,因此它是一种非常灵活的数据结构。
-
可以在常数时间内执行队列头部或尾部的插入和删除操作。
-
可以用于解决一些需要同时从队列头部和尾部进行操作的问题,例如维护滑动窗口的最大值、最小值等。
需要注意的是,双端队列的实现可以采用数组或链表等数据结构。采用数组实现的双端队列需要在插入和删除时移动元素,因此插入和删除的时间复杂度为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中,树是一种非常常用的数据结构,它由一个或多个节点组成,节点之间通过边连接。
优点:
-
能够快速地查找、插入和删除元素。
-
能够进行高效的排序和搜索操作,例如二叉搜索树(BST)。
-
能够进行快速的合并和分割操作,例如堆。
缺点:
-
树的结构比较复杂,实现起来相对困难。
-
树的平衡性需要得到保证,否则可能会导致树的深度过大,影响性能。
-
树的节点数量不易控制,如果节点数量过多,可能会占用大量内存。
需要根据具体的使用场景来选择树这种数据结构,对于一些需要进行高效的查找、排序、合并和分割操作的问题,树是一种非常合适的数据结构。
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
}