一、数组
1. 特性
数组是存放在连续内存空间上的相同类型数据的集合。(区别于 TypeScript 中的元组Tuple, 元组中的数据类型可以不同)
从数组在计算机的存储上来看,数组具有以下两个特点:
- 数组下标都是从0开始的
- 数组内存空间的地址是连续的
因为数组在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。
2. 时间复杂度
数组操作的时间复杂度分别如下所示:
- 查找:O(1)
- 插入:O(n)
- 删除:O(n)
- 增加:O(n)
时间复杂度是指随着输入数据的增多,其时间的线性变化情况。
- 在开始下标插入O(n),在末尾下标插入O(1),取平均值为O(n/2)近似于O(n)
- 在开始下标删除O(n),在末尾下标删除O(1),取平均值为O(n/2)近似于O(n)
3. 二维数组
二维数组是一种结构较为特殊的数组,只是将数组中的每个元素变成了一维数组。二维数组的本质上仍然是一个一维数组,内部的一维数组仍然从索引0开始,我们可以将它看作一个矩阵,利用二维数组可以处理矩阵相关的问题,如矩阵旋转、对角线遍历,以及对子矩阵的操作等。
对于一个二维数组 A = [[1, 2, 3, 4],[2, 4, 5, 6],[1, 4, 6, 8]],计算机同样会在内存中申请一段连续的空间,并记录第一行数组的索引位置,即 A[0][0] 的内存地址,它的索引与内存地址的关系如下图所示
4. 算法应用
- 原地移除数组元素
var removeElement = function(nums, val) {
let slow = 0;
for(let fast=0;fast<nums.length;fast++){
if(nums[fast] !== val){
nums[slow] = nums[fast];
slow++;
}
}
return slow;
};
- 螺旋矩阵
var generateMatrix = function(n) {
let startX = 0,startY = 0,offset = 1;
let loop = Math.floor(n/2);
let mid = Math.floor(n/2);
let count = 1;
let res = Array(n).fill(0).map(()=> new Array(n).fill(0));
while(loop--){
let row = startX;
let clo = startY;
// 从左到右
for(;clo < startY+n-offset;clo++){
res[row][clo] = count ++;
}
// 从上到下
for(;row < startX+n-offset;row++){
res[row][clo] = count ++;
}
// 从右到左
for(;clo > startY;clo--){
res[row][clo] = count ++;
}
// 从下到上
for(;row > startX;row--){
res[row][clo] = count ++;
}
startX++;
startY++;
offset += 2;
}
if(n % 2 === 1){
res[mid][mid] = count;
}
return res;
};
二、字符串
1. 特性
字符串是具有顺序的字符线性结构,但相比普通数组,它具有更强的语义、更复杂的存储与操作成本,并且在多数高级语言中是不可变的,因此很多看似简单的操作实际上是 O(n)。
2. 时间复杂度
字符串的大多数“增删改”,本质都是 O(n),查找时间复杂度为o(1)
3. 算法应用
- 原地反转字符串
🤔解题思路:
1️⃣创建两个指针,分别从字符串头部和尾部开始
2️⃣for遍历字符串,交换两个指针的数值
var reverseString = function(s) {
if(s.length <= 1){
return s;
}
let l = 0,r = s.length - 1;
while(l < r){
let mid = s[l];
s[l] = s[r];
s[r] = mid;
l++;
r--;
}
};
三、链表
1. 特性
链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。
链表的入口节点称为链表的头结点head。
链表分为单链表、双链表和循环链表。
-
单链表:指针域只能指向节点的下一个节点
-
双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。既可以向前查询也可以向后查询。
- 循环链表:链表首尾相连
链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。
2. 时间复杂度
以单链表为例的时间复杂度如下:
- 查找:O(n)
- 插入:O(1)
- 删除:O(1)
- 增加:O(1)
3. 算法应用
- 移除链表元素
var removeElements = function(head, val) {
const newHead = new ListNode(-1,head);
let cur = newHead;
while(cur.next !== null){
if(cur.next.val === val){
cur.next = cur.next.next;
}else{
cur = cur.next;
}
}
return newHead.next;
};
- 反转链表
var reverseList = function(head) {
if(!head || !head.next) return head;
let pre = null,cur = head;
while(cur){
const node = cur.next;
cur.next = pre;
pre = cur;
cur = node;
}
return pre;
};
- 环形链表
🤔解题思路:
1️⃣初始化快慢指针:slow走一步,fast走两步
2️⃣确定循环条件fast !== null && fast.next !== null
3️⃣判断是否有环fast === slow
4️⃣获取环的入口节点:同步指针分别从头节点和相遇节点开始前进,再次相遇的节点即为入口节点
var detectCycle = function(head) {
let fast = head,slow = head;
while(fast !== null && fast.next !== null){
fast = fast.next.next;
slow = slow.next;
// 快慢指针相遇表示有环,需要记录当前相遇节点
if(fast === slow){
let index1 = fast;
let index2 = head;
// 相遇节点和头节点以相同速度后移,两者相遇的节点即为环入口节点
while(index1 !== index2){
index1 = index1.next;
index2 = index2.next;
}
return index1;
}
}
return null;
};
四、Map映射
1. 特性
Map(也叫字典、哈希表)是键值对集合,大部分Map都是无序的,且键唯一性。Map提供以下操作:
- set(key, value):插入或更新
- get(key):根据 key 获取 value
- delete(key):删除 key
- has(key):判断 key 是否存在
- keys()/values()/entries():遍历
2. 时间复杂度
- 查:O(1)
- 增:O(1)
- 删:O(1)
- 改:O(1)
- 遍历:O(n)
3. 算法应用
- 字母异位词分组
var groupAnagrams = function(strs) {
const map = new Map();
for(let s of strs){
let array = s.split('');
array.sort();
let key = array.join('');
let value = map.get(key) || new Array();
value.push(s);
map.set(key,value);
}
return Array.from(map.values());
};
- 有效的字母异位词
var isAnagram = function(s, t) {
if(s.length !== t.length) return false;
let s_map = {},t_map = {};
for(let c of s){
s_map[c] = (s_map[c] || 0) + 1;
}
for(let c of t){
t_map[c] = (t_map[c] || 0) + 1;
}
for(let i in s_map){
if(s_map[i] !== t_map[i]){
return false;
}
}
return true;
};
五、栈 & 队列
1. 特性
队列是先进先出、栈是先进后出的数据结构。
2. 时间复杂度
栈和队列的时间复杂度如下:
| 类型 | 查找 | 插入 | 删除 |
|---|---|---|---|
| 栈 | O(n) | O(1) | O(1) |
| 队列 | O(n) | O(1) | O(1) |
3. 算法应用
- 有效的括号
var isValid = function(str) {
const chartMap = {
'(':')',
'[':']',
'{':'}'
}
let stack = [];
for(let s of str){
if(chartMap[s]){
stack.push(s);
}else{
const cur = stack.pop();
if(chartMap[cur] !== s){
return false;
}
}
}
return stack.length === 0;
};
- 删除相邻重复项
var removeDuplicates = function(s) {
const stack = [];
for(let i=0;i<s.length;i++){
if(stack.length === 0){
stack.push(s[i]);
}else{
const c = stack.pop();
if(c === s[i]){
continue;
}else{
stack.push(c);
stack.push(s[i]);
}
}
}
return stack.join('');
};
- 滑动窗口最大值
🤔解题关键:
求的是滑动窗口的最大值,如果当前的滑动窗口中有两个下标i和j,其中i在j的左侧(i<j),并且i对应的元素不大于j对应的元素(nums[i]≤nums[j]),当滑动窗口向右移动时,只要i还在窗口中,那么j一定也还在窗口中。因此,由于 nums[j]的存在,nums[i]一定不会是滑动窗口中的最大值,所以可以将 nums[i]永久地移除。
🤔解题思路:
1️⃣初始化一个队列存储所有还没有被移除的下标
2️⃣for循环数组,模拟滑动窗口行为
3️⃣判断队列前几位的大小,踢出小的元素
4️⃣每移动一次窗口就获取一次最大值
var maxSlidingWindow = function (nums, k) {
let queue = [],result = [];
for (let i = 0; i < nums.length; i++) {
if (i >= k && queue[0] <= i - k) {
queue.shift();
}
while (queue.length && nums[queue[queue.length - 1]] < nums[i]) {
queue.pop();
}
queue.push(i);
if (i >= k - 1) {
result.push(nums[queue[0]]);
}
}
return result;
};
六、优先队列
1. 特性
Heap数据结构的特点是按照顺序进入队列,按照优先级出队列。进入队列的数据需要记录优先级。
Heap可以分为以下几种:
- Mini Heap:堆顶元素最小
- Max Heap:堆顶元素最大
以二叉树为例的小顶堆
以二叉树为例的大顶堆
2. 时间复杂度
3. 算法应用
- 数组第K个最大元素
var findKthLargest = function(nums, k) {
let heapSize = nums.length;
// 构建大顶堆
buildMaxHeap(nums,heapSize);
// 进行下沉,最大元素下沉到末尾
for(let i=nums.length-1;i>=nums.length-k+1;i--){
// 交换元素
swap(nums,0,i);
--heapSize;
// 从左到右、自上而下调整节点
maxHeapify(nums,0,heapSize);
}
return nums[0];
};
var buildMaxHeap = (nums,heapSize)=>{
for(let i=Math.floor(heapSize/2) - 1;i>=0;i--){
maxHeapify(nums,i,heapSize);
}
}
var maxHeapify = (nums,i,heapSize)=>{
let l = i*2+1;
let r = i*2+2;
let largest = i;
if(l < heapSize && nums[l] > nums[largest]){
largest=l;
}
if(r < heapSize && nums[r] > nums[largest]){
largest=r;
}
if(largest !== i){
swap(nums,i,largest);
maxHeapify(nums,largest,heapSize);
}
}
var swap = (a,i,j)=>{
let temp = a[i];
a[i] = a[j];
a[j] = temp;
}
七、树
1. 特性
树是一种非线性的数据结构,由n(n>= 0)个节点组成的集合。
- n = 0:空树
- n > 0:有一个根节点root,根节点没有父节点;根节点以外的其他元素被分为m个互不相交的集合T1,T2,T3,...Tm-1,其中每一个集合Ti本身就是一棵树,叫做原树的子树
- 节点
每个节点都包含一个数据项和一个指向其他节点的指针(上图1-11都是节点)
- 节点的度
一个节点包含指向其他节点的指针个数
- 叶节点
节点度为0的节点是叶节点
- 分支节点
节点度不为0的节点是分支节点
- 子女节点
若节点X有子树,则子树的根节点就是节点X的子女节点,例如2,3,4都是1的子女节点
- 父节点
若节点X有子女节点,则节点X就是子女节点的父节点,例如1是2,3,4的父节点
- 兄弟节点
同一个父节点的子女节点,互相为兄弟节点,例如2,3,4是兄弟节点
- 祖先节点
从根节点到当前节点为止,经过的所有节点,例如11的祖先节点是1,2,6
- 子孙节点
一个节点的子女节点、子女节点的子女节点即为该节点的子孙节点,例如2的子孙节点是5,6,11
- 节点所在层次
根节点root在第一层,其子女节点在第二层,以此类推
- 树的深度
一棵树中距离根节点root最远的节点所在的层次,上述例子中的深度为4
- 树的高度
叶节点的高度为1,非叶节点的高度是其子女节点高度的最大值+1,高度和深度的数值相等,但意义和计算方式不相同
- 树的度
一棵树中所有节点的度的最大值,例子中的树的度是3
- 有序树
一棵树所有节点的子树是按照顺序排列的,T1是第一颗子树,T2是第二颗子树..
- 无序树
一棵树所有节点的子树的顺序不重要,可以相互交换位置
- 森林
森林是M棵树的集合,M>=0
2. 二叉树
- 特点
- 每个节点最多有两个子女节点,分别称为左子女和右子女
- 二叉树中不存在度大于2的节点
- 二叉树的左右子树有次序之分,不能颠倒
二叉树的第n(n>=1)层最多有2的n-1次方个节点
- 特殊二叉树
- 满二叉树
如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。
- 完全二叉树:除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第h层,则该层包含 1~ 2^(h-1)个节点。
- 二叉搜索树:左子树上所有结点的值均小于它的根结点的值,右子树上所有结点的值均大于根结点的值,且左、右子树也分别为二叉搜索树。
- 平衡二叉搜索树:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
二叉树可以链式存储,也可以顺序存储。链式存储可以用指针实现,顺序存储可以用数组实现。
二叉树主要有两种遍历方式:
- 深度优先遍历:先往深走,遇到叶子节点再往回走
- 前序遍历(递归法,迭代法)
- 中序遍历(递归法,迭代法)
- 后序遍历(递归法,迭代法)
- 广度优先遍历:一层一层的去遍历
- 层次遍历(迭代法)
定义节点类:
const BinTreeNode = function(data){
this.data = data; // 节点数据项
this.leftChild = null; // 左子女节点
this.rightChild = null; // 右子女节点
this.parentNode = null; // 父节点
}
3. 算法应用
var preorderTraversal = function(root) {
const arr = [];
var prevTraversal = function(root){
if(!root)return;
arr.push(root.val);
prevTraversal(root.left);
prevTraversal(root.right);
}
prevTraversal(root);
return arr;
};
var postorderTraversal = function(root) {
const arr = [];
var postTraversal = (root)=>{
if(root === null)return;
const left = postTraversal(root.left);
const right = postTraversal(root.right);
arr.push(root.val);
}
postTraversal(root);
return arr;
};
var inorderTraversal = function(root) {
const arr = [];
var inTraversal = function(node){
if(!node)return;
inTraversal(node.left);
arr.push(node.val);
inTraversal(node.right);
}
inTraversal(root);
return arr;
};
var levelOrder = function(root) {
let arr = [],queue = [root];
if(root === null){
return arr;
}
while(queue.length){
// 存放每一层的节点
let curLevel = [];
let length = queue.length;
for(let i=0;i < length;i++){
let node = queue.shift();
curLevel.push(node.val);
if(node.left)queue.push(node.left);
if(node.right)queue.push(node.right)
}
// 把每一层的结果放入数组中
arr.push(curLevel);
}
return arr;
};
var invertTree = function(root) {
if(!root) return null;
const left = invertTree(root.left);
const right = invertTree(root.right);
root.left = right;
root.right = left;
return root;
};
var isSymmetric = function(root) {
if(!root) return true;
var compare = function(left,right){
if(!left && !right)return true;
if(left && right && left.val === right.val){
return compare(left.left,right.right) && compare(left.right,right.left)
}
return false;
}
return compare(root.left,root.right);
};