数据结构
数据结构指的是数据之间的相互关系,它一般包括以下三个方面的内容:
- 数据的逻辑结构:数据之间的逻辑关系
- 数据的存储结构:数据元素及其关系在计算机存储器内的表示
- 数据的运算,即对数据进行的操作
逻辑结构
根据数据逻辑关系的不同,可分为四种基本结构类型
- 集合 :数据具有符合某一条件的相同的性质,且别无其他关系。
- 线性结构:数据之间存在一对一的关系。
- 树形结构:数据之间存在一对多的关系。
- 图形结构:数据之间存在多对多的关系。
存储结构
存储方法分类:
- 顺序存储方法: 用一组地址连续的存储单元依次存储线性表的各个数据元素
- 链式存储方法:用一组任意的存储单元存储线性表中的各个数据元素
概览
线性结构
线性表
零个或多个数据元素的有限序列。
顺序存储结构
把线性表的各个数据元素依次存储在一组地址连续的存储单元里 ;用这种方法存储的线性表简称为顺序表
链式存储结构
用一组任意的存储单元来存储线性表的数据元素
数据域(Data Field)
:存储数据元素信息的域
指针域(Link Field)
:存放直接后继结点或前驱结点地址的域
栈
栈是一种遵从先进后出 (LIFO) 原则的有序集合;新添加的或待删除的元素都保存在栈的末尾,称作栈顶,另一端为栈底。
队列
队列是一种先进先出(FIRST IN FIRST OUT 简称FIFO)的线性表;只允许在表的一端进行插入,在另一端删除元素;允许插入的一端称为队尾(Rear),允许删除的一端称为队头(Front)
优先队列
优先队列的思想
是将最重要的元素赋予最小的优先级(从代价角度)或最大的优先级(从利润角度)
链表
链表存储有序的元素集合,但不同于数组,链表中的元素在内存中并不是连续放置的。每个 元素由一个存储元素本身的节点和一个指向下一个元素的引用(也称指针或链接)组成。
非线性结构
树
包含n(n>0)个节点的有穷集合K,且在K中定义了一个关系N,N满足以下条件:
- 有且仅有一个结点K0,它对于关系N来说没有前驱,称K0为树的根结点
- 除K0外,K中的每个结点对于关系N来说有且仅有一个前驱
- K中各结点对关系N来说可以有m个后继(m≥0)
二叉树
堆(特殊二叉树)
堆的特性:
- 必须是完全二叉树
- 用数组实现
- 任一结点的值是其子树所有结点的最大值或最小值
- 最大值时,称为“最大堆”,也称大顶堆;
- 最小值时,称为“最小堆”,也称小顶堆。
图
图是网络结构的抽象模型。图是一组由边连接的节点(或顶点),任何二元关系都可以用图来表示。
哈希表
哈希的基本原理是将给定的键值转换为偏移地址来检索记录。
键转换为地址是通过一种关系(公式)来完成的,这就是哈希(散列)函数。
算法
时间复杂度
把算法中基本操作重复执行的次数(频度)作为算法的时间复杂度。
没有循环语句,记作O(1)
,也称为常数阶。只有一重循环,则算法的基本操作的执行频度与问题规模n呈线性增大关系,记作O(n)
,也叫线性阶。
常见的时间复杂度有:
O(1)
: Constant Complexity: Constant 常数复杂度O(log n)
: Logarithmic Complexity: 对数复杂度O(n)
: Linear Complexity: 线性时间复杂度O(n^2)
: N square Complexity 平⽅方O(n^3)
: N square Complexity ⽴立⽅方O(2^n)
: Exponential Growth 指数O(n!)
: Factorial 阶乘
空间复杂度
算法所需存储空间的度量
基础算法
排序与查找算法
自己刚入门前端练习的算法
冒泡排序
//--------------------------
for(var i=0,len=arr.length;i<len;i++){
for(var j=i+1;j<len;j++){
if(arr[i]>arr[j]){ //将最大的放在最后
var temp=0;
t=arr[i];
arr[i]=arr[j];
arr[j]=temp;
}
}
}
//--------第二种(改善)------------
for(var i=1,len=arr.length;i<len;i++){
for(var j=0;j<len-i;j++){ //已经排序好的最大数组不必在比较
if(arr[j]>arr[j+1]){
var temp=0;
t=arr[i];
arr[i]=arr[j];
arr[j]=temp;
}
}
}
选择排序
for(var i=0,len=arr.length-1;i<len;i++){
int min = i;
for(var j=i+1;j<len;j++){
if(arr[min]>arr[j]){
min=j;
}
}
if(min!=i){
var temp=0;
t=arr[min];
arr[min]=arr[i];
arr[i]=temp;
}
}
直接插入排序
for (int i = 1; i < arr.Length; i++){
if (arr[i - 1] > arr[i])
{
int temp = arr[i]; //每次取出当前的要比较的值
int j = i;
while (j > 0 && arr[j - 1] > temp) //大于它的话,要赋值给它
{
arr[j] = arr[j - 1];
j--;
}
arr[j] = temp;
}
}
快速排序
//-----------------first---------------
var quickSort = function(arr){
var arr = arr.concat(); //concat的新用法(深复制)
if(arr.length<=1) return arr;
var index = Math.floor(arr.length/2);
var centerValue = arr.splice(index,1);
//console.log(centerValue);
var left = [];
var right = [];
for(var i=0,len=arr.length;i<len;i++){
if(centerValue>=arr[i]){
left.push(arr[i]);
}else{
right.push(arr[i]);
}
}
//console.log(quickSort(left));
//console.log(right);
var res1 = arguments.callee(left);
var res2 = arguments.callee(right);
//return left.concat(right);
return res1.concat(centerValue,res2);
}
var arr=[9,8,7,4,5,3,77];
var result = quickSort(arr);
console.log(result);
console.log(arr);
//-----------------second---------------
var quickSort = function(arr){
if(arr.length<=1) return arr;
var index = Math.floor(arr.length/2);
console.log(index); //1
var centerValue = arr.slice(index,index+1)[0];
console.log(centerValue); //5
var left = [];
var right = [];
for(var i=0,len=arr.length;i<len;i++){
if(centerValue>=arr[i]){
left.push(arr[i]);
}else{
right.push(arr[i]);
}
}
var res1 = arguments.callee(left);
var res2 = arguments.callee(right);
return res1.concat(res2);
}
var arr=[9,8,7,4,5,3,4,77];
//var result = quickSort(arr);
//console.log(result);
console.log(arr);
var aa=[3,4];
//console.log(aa.slice(1,2));
console.log(aa.splice(0,0));
反转排序
for(var i=0,len=arr.length-1;i<len/2;i++){
var temp=0;
t=arr[i];
arr[i]=arr[len-1-i];
arr[len-1-i]=temp;
}
参数排序
function mySort() {
var tags = new Array();//使用数组作为参数存储容器
tags = Array.prototype.slice.call(arguments);
tags.sort(function(a,b){
return a-b;
});
return tags;//返回已经排序的数组
}
var result = mySort(50,11,16,32,24,99,57,100); //传入参数个数不确定
console.info(result); //显示结果
二分查找
--迭代--
var searchInsert = function(nums, target) {
let l = 0
let r = nums.length - 1
while(l <= r) {
let mid = l + ((r - l) >> 1)
if (nums[mid] === target) {
return mid
} else if (nums[mid] < target) {
l = mid + 1
} else if (nums[mid] > target) {
r = mid - 1
}
}
return -1
};
--递归--
var searchInsert = function(nums, target) {
function binarySearch(l, r) {
if (l <= r) {
const mid = l + ((r - l) >> 1)
if (nums[mid] === target) {
return mid
} else if (nums[mid] < target) {
return binarySearch(mid + 1, r)
} else if (nums[mid] > target) {
return binarySearch(l, mid - 1)
}
}
return -1
}
return binarySearch(0, nums.length - 1)
};
动态规划
Redraiment是走梅花桩的高手。Redraiment可以选择任意一个起点,从前到后,但只能从低处往高处的桩子走。他希望走的步数最多,你能替Redraiment研究他最多走的步数吗?
输入:
6
2 5 1 5 4 5
输出:
3
说明:
6个点的高度各为 2 5 1 5 4 5
如从第1格开始走,最多为3步, 2 4 5 ,下标分别是 1 5 6
从第2格开始走,最多只有1步,5
而从第3格开始走最多有3步,1 4 5, 下标分别是 3 5 6
从第5格开始走最多有2步,4 5, 下标分别是 5 6
所以这个结果是3。
function maxStep(arr) {
let max = 0
const temp = new Array(arr.length)
for(let i = 0; i<arr.length; i++) {
temp[i] = 1
for(let j =0;j<i;j++) {
if(arr[j] < arr[i]) {
temp[i] = Math.max(temp[j] + 1, temp[i])
}
}
max = Math.max(temp[i] , max)
}
return max
}
贪心算法
是指在对问题进行求解时,在每一步选择中都采取最好或者最优(即最有利)的选择
,从而希望能够导致结果是最好或者最优的算法
算法所得到的结果不一定是最优的结果(有时候会是最优解),但是都是相对近似(接近)最优解的结果
55. 跳跃游戏 主要思路为:如果一个位置能够到达,那么这个位置左侧所有位置都能到达
var canJump = function(nums) {
const len = nums.length
let max = 0
for(let i =0;i<len;i++) {
if (i > max) {
return false
}
max = Math.max(i+nums[i], max)
if (max >= len -1) return true
}
return false
}
45. 跳跃游戏 II 主要思路:跳跃最小次数,反向就是每次找到可到达的最远位置
var jump = function(nums) {
if (nums.length <= 1) {
return 0
}
let max = 0
let step = 0
let end = 0
for(let i = 0;i<nums.length;i++) {
max = Math.max(max, i+nums[i])
if (max >= nums.length - 1) {
return step+1
}
if (i === end) {
step++
end = max
}
}
return step
}
动态归划
var jump = function (nums) {
var len = nums.length
var dp = []
dp[0] = 0
for (var i = 1; i < len; i++) {
dp[i] = Number.MAX_VALUE
for (var j = 0; j < i; j++) {
if (j + nums[j] >= i) {
dp[i] = Math.min(dp[i], dp[j] + 1)
}
}
}
return dp[len - 1]
}
二叉树
先序遍历
(根左右)
后序遍历
(左右根)
中序遍历(左根右)
递归
(root) => {
const res = [];
const inorder = (root) => {
if (root == null) {
return;
}
inorder(root.left);
res.push(root.val);
inorder(root.right);
};
inorder(root);
return res;
};
迭代
(root) => {
const res = [];
const stack = [];
while (root) { // 能压栈的左子节点都压进来
stack.push(root);
root = root.left;
}
while (stack.length) {
let node = stack.pop(); // 栈顶的节点出栈
res.push(node.val); // 在压入右子树之前,处理它的数值部分(因为中序遍历)
node = node.right; // 获取它的右子树
while (node) { // 右子树存在,执行while循环
stack.push(node); // 压入当前root
node = node.left; // 不断压入左子节点
}
}
return res;
};
层序遍历
function(root) {
const res = []
if (!root) {
return res;
}
const q = []
q.push(root)
while(q.length) {
res.push([])
const len = q.length
for(let i =0;i<len;i++) {
const node = q.shift()
res[res.length - 1].push(node.val)
if (node.left) q.push(node.left)
if (node.right) q.push(node.right)
}
}
return res
}
DFS
DFS可谓是“不撞南墙,不回头”。就像走迷宫一样,选择一条路一直走下去,当撞南墙了,才返回分叉口去试下一条路。
具体算法描述为:
选择一个起始点
作为 当前结点,执行如下操作:
a. 访问 当前结点,并且标记该结点已被访问,然后跳转到 b;
b. 如果存在一个和 当前结点 相邻并且尚未被访问的结点,则将
设为 当前结点,继续执行 a;
c. 如果不存在这样的,则进行回溯,回溯的过程就是回退 当前结点;\
上述所说的 当前结点 需要用一个栈来维护,每次访问到的结点入栈,回溯的时候出栈。除了栈,另一种实现深度优先搜索的方式是递归。
BFS
广度优先遍历呈现出「一层一层向外扩张」的特点,先看到的结点先遍历,后看到的结点后遍历,因此「广度优先遍历」可以借助「队列」实现。
- 首先将节点加入队列Q。
- 访问节点,同时将节点设置为已经被访问。
- 循环从Q中取出节点:
- 如果Q为空,则遍历结束。
- 如果Q不为空,将该节点设置为已经访问。同时将其子节点加入Q进行排队。要观察子节点是否已经被访问或者已经加入了队列Q,否则将出现死循环,或者重复遍历。
双指针
双指针技巧在处理数组和链表相关问题时经常用到,主要分为两类:左右指针和快慢指针。
所谓左右指针,就是两个指针相向而行或者相背而行;而所谓快慢指针,就是两个指针同向而行,一快一慢。
对于单链表来说,大部分技巧都属于快慢指针。 5. 最长回文子串
中心扩散法(左右指针)
var longestPalindrome = function(s) {
let result = ''
for(let i = 0; i<s.length; i++) {
const odd = palindromic(s, i, i)
const even = palindromic(s, i, i+1)
const maxStr = odd.length > even.length ? odd : even
result = result.length > maxStr.length ? result : maxStr
}
return result
};
function palindromic(s, l, r) {
while(l>=0 && r<s.length && s[l] === s[r]) {
l--
r++
}
return s.slice(l+1, r)
}
动态规划
function palindrome(s) {
const n = s.length;
let dp = Array.from(new Array(n), () => new Array(n).fill(false));
let [left, right] = [0, 1];
for (let i = 1; i < n; i++) {
for (let j = 0; j < i; j++) {
if (s[i] === s[j]) {
if (i - j < 3) {
dp[i][j] = true;
} else {
dp[i][j] = dp[i - 1][j + 1];
}
}
if (dp[i][j] && i - j + 1 > right - left) [left, right] = [j, i + 1];
}
}
return s.slice(left, right)
}
快慢双指针
function(nums1, m, nums2, n) {
let p1 = 0, p2 = 0;
const sorted = [];
var cur;
while (p1 < m || p2 < n) {
if (p1 === m) {
sorted.push(nums2[p2++])
} else if (p2 === n) {
sorted.push(nums1[p1++])
} else if (nums1[p1] < nums2[p2]) {
sorted.push(nums1[p1++])
} else {
sorted.push(nums2[p2++])
}
}
for (let i = 0; i != m + n; ++i) {
nums1[i] = sorted[i];
}
};
滑动窗口
一种较难掌握的双指针技巧
主要思路是维持一个窗口,满足一定条件下,进行右滑扩大窗口,或者左滑缩小窗口 209. 长度最小的子数组
滑动窗口
var minSubArrayLen = function(target, nums) {
let l = 0
let r = 0
const len = nums.length
let sum = 0
let min = Infinity
while(r < len) {
sum += nums[r]
while(target <= sum) {
min = Math.min(min, r-l+1)
sum -= nums[l++]
}
r++
}
return min === Infinity ? 0: min
};
前缀和
核心思路是我们 new 一个新的数组preSum
出来,preSum[i]
记录nums[0..i-1]
的累加和:
560. 和为 K 的子数组
前缀和 + 哈希
var subarraySum = function(nums, k) {
let count = 0
let map = new Map([[0, 1]])
const len = nums.length
let sum = 0
for (let i =0;i<len;i++) {
sum += nums[i]
if (map.has(sum - k)) {
count += map.get(sum - k)
}
if (map.has(sum)) {
map.set(sum, map.get(sum) + 1)
} else {
map.set(sum, 1)
}
}
return count
};
类似的三数之和,是使用排序+双指针
差分数组
核心思路是构造一个diff
数组,值为nums[i]-nums[i-1]