数据结构:计算机存储或者组织数据的方式
算法: 解决问题的方式
时间复杂度:
1. 是什么?
执行当前算法所"花费的时间"
2. 干什么?
写代码的过程中,就可以大概知道代码运行的快与慢
3. 表示
大O表示法: 《解析数论》
常用时间:O(1) O(n) O(n^2) O(logn)
O(n),
也叫线性时间,这样的算法包括简单查找。
O(log n),
也叫对数时间,这样的算法包括二分查找。
O(n * log n),
这样的算法包括第4章将介绍的快速排序——一种速度较快的排序算法。
O(n2 ),
这样的算法包括第2章将介绍的选择排序——一种速度较慢的排序算法。
O(n!),
这样的算法包括接下来将介绍的旅行商问题的解决方案——一种非常慢的算法。
空间复杂度:
1. 是什么?
执行当前算法需要占用多少内存空间
2. 表示法
常用空间:O(1) O(n) O(n^2) O(logn)
栈、队列
都是进栈、出栈
栈
特点:后进先出
入:push [1,2,3,4]、
出:pop [1,2,3] ==> 4
力扣20有效的括号
var isValid = function (s) {
var stack = [];
for (let i = 0; i < s.length; i++) {
const start = s[i];
if (s[i] == '(' || s[i] == '{' || s[i] == '[') {
stack.push(s[i]);
} else {
const end = stack[stack.length - 1];
if (start == ")" && end == '(' ||
start == "}" && end == '{' ||
start == "]" && end == '['
) {
stack.pop()
} else {
return false
}
}
}
return stack.length == 0
};
console.log(isValid('()[]{}'));
console.log(isValid('()[]{]'));
console.log(isValid('(){]{]'));
力扣1047删除字符串中的所有相邻重复项
var removeDuplicates = function(s) {
let stack = [];
for( v of s ){
let prev = stack.pop();
if( prev != v ){
stack.push( prev );
stack.push( v );
}
}
return stack.join('');
};
console.log( removeDuplicates('abbaca') );
力扣71 简化路径
var simplifyPath = function(path) {
let stack = [];
let str = '';
let arr = path.split('/');
arr.forEach( val=>{
if( val && val == '..' ){
stack.pop();
}else if( val && val != '.'){
stack.push( val );
}
})
arr.length ? str = '/' + stack.join('/') : str = '/';
return str;
};
console.log( simplifyPath('/home//foo/') );
队列
队列 : 先进先出 let arr = [];
arr.push(1);
arr.push(2);
console.log( arr );
arr.shift();
console.log( arr );
任务队列
了解事件机制,方便后续的题目讲解
同步 、 异步(定时器、事件、请求...)
异步分为:宏任务( 定时器 )和 微任务(promise.then)
JS执行流程
-
主线程读取JS代码,此时同步环境,形容对应的堆和执行栈
-
主线程遇到异步任务,会推给异步线程进行处理
-
异步进行处理完毕,将对应的异步任务推入任务队列
-
主线程查询任务队列,执行微任务,将其按照顺序执行,全部执行完毕。
-
主线程查询任务队列,执行宏任务,取得第一个宏任务,执行完毕。
-
重复以上4,5步骤。
Promise.resolve('3333').then(res=>{
console.log( res );
setTimeout(()=>{
console.log('Promise setTimeout')
},0)
})
setTimeout(()=>{
console.log( 111 );
Promise.resolve('setTimeout Promise').then(res=>{
console.log( res );
})
},0)
console.log( 222 );
力扣933最近的请求次数
链表、数组
多个元素存储组成的
·
一、什么是链表
-
多个元素存储的列表
-
链表中的元素在内存中不是顺序存储的,而是通过"next"指针联系在一起的。
***js中的
原型链原理就是链表结构解释next指针:如下 { val:'a', next:{ val:'b', next:{ val:'c', next:{ val:'d', next:null } } } }对链表的操作(插入、删除)都是通过指针来进行的:如下
//遍历链表 let obj = a; while( obj && obj.key ){ console.log( obj.key ); obj = obj.next; } //链表中插入某个元素 let m = {key:'mmmmm'}; c.next = m; m.next = d; console.log( a ); //删除操作 c.next = d;
二、链表和数组的区别
1. 数组:有序存储的,在中间某个位置删除或者添加某个元素,其他元素要跟着动。
2. 链表中的元素在内存中不是顺序存储的,而是通过"next"指针联系在一起的。
三、链表分类
单向链表 (1个指针)
双向链表 (2个指针)
环形链表
instanceof原理
const instanceofs = (target,obj)=>{
let p = target;
while( p ){
if( p == obj.prototype ){
return true;
}
p = p.__proto__;
}
return false;
}
console.log( instanceofs( [1,2,3] , Object ) )
力扣237 删除链表中的节点
/** 链表中的元素是通过指针next指向下一个元素,一级一级找,只到null.
* @param {ListNode} node 当前节点
* @return {ListNode}
*/
var deleteNode = function(node) {
node.val = node.next.val;//当前节点的值等于下一个节点
node.next = node.next.next;//当前节点的指针指向下下个节点
};
力扣237 删除链表中的重复元素
/** 链表中的元素是通过指针next指向下一个元素,一级一级找,只到null.
* @param {ListNode} head 头节点
* @return {ListNode}
*/
var deleteDuplicates = function(head) {
if(!head) return head;//从头节点开始找
let cur = head;
while(cur.next){
//当前节点的值与下一个节点的值相等,表示节点重复。 重复则用下个节点的覆盖前一个节点
if(cur.val == cur.next.val){
cur.next = cur.next.next
}else{
cur = cur.next
}
}
return head;
}
力扣206 反转链表
/** 思路:将链表的 头尾节点互换
* @param {ListNode} head 链表头节点
* @return {ListNode}
*/
var reverseList = function (head) {
let prev = null; //头节点
let cur = head;//尾节点
while (cur) {
const next = cur.next; //将当前的节点指针存储
cur.next = prev;//把尾节点赋值到头节点
prev = cur;
cur = next;
}
return prev;
};
环形链表
指针的指向永远不为空
var hasCycle = function(head){
// f,s是2个指针
let f= head,s=head;
// f不为null,f.next不为null,就一直循环
while(f!=null && f.next!=null){
s = s.s.next;//当前的指针
f= f.next.next;//下一个指针
if(s==f)return true
}
return false;
}
字典
前提知识:对象的键是字符串,若键重名,则后面的覆盖前面的。
字典 : 键值对存储的,类似于js的对象(键[key]都是字符串类型或者会转换成字符串类型)
核心:字典 是通过map来表示的,map的键不会转换类型
var obj = {
a:1,
a:2
}
console.log(a) //2 键名一样,后面的覆盖前面的
var a = {}
var b = {
key:'a'
}
var c = {
key:'c'
}
a[b] = '123';
a[c] = '456';
//键名相同,后一个会覆盖前一个 ,通过对象obj 理解
console.log( a[b] ); //456
哈希表 【散列表】
理解:哈希表 类似 电话簿,其中每个姓名都有对应的电话号码。查找张三的电话号码,为此只需向散列表传入相应的键张三。
-- 无需查找,只需根据键取
在js中没有哈希表,哈希表是字典一种实现。
字典与哈希表区别
区别一:如果找key对应的value需要遍历key,那么想要省去遍历的过程,用哈希表来表示。
区别二:排列顺序
-
字典是根据添加的顺序进行排列的
-
哈希表是随机进行排列的
力扣01两数之和
力扣217存在重复元素
力扣349两个数组的交集
力扣1237 独一无二的出现次数
面试:统计一个字符串中出现次数最多的字符
力扣03无重复字符的最长子串
深度优先遍历 与 广度优先遍历
eg: 如何通过深度优先遍历 与 广度优先遍历实现下面树结构如下:
const tree = {
val:'a',
children:[
{
val:'b',
children:[
{val:'d',children:[]},
{val:'e',children:[]},
]
},
{
val:'c',
children:[
{val:'f',children:[]},
{val:'g',children:[]}
]
}
]
}
一、深度优先搜索(遍历)
概念:
从根出发,尽可能深的搜索树的节点
技巧:(递归实现)
-
访问根节点
-
对根节点的children挨个进行深度优先搜索
代码
//深度优先遍历
const fun1 = (root)=>{
console.log( root.val );
root.children.forEach( fun1 );
}
fun1(tree);
二、广度优先搜索(遍历)
概念
从根出发,优先访问离根节点最近的节点
技巧:(出栈进栈实现)
1. 新建一个队列,把根节点入队
2. 把队头出队
3. 把队头的children挨个入队
4. 重复2和3步,直到队列为空为止
代码
//广度优先遍历
const fun2 = (root)=>{
const arr = [root];
while( arr.length > 0 ){
const o = arr.shift();
console.log( o.val );
o.children.forEach(item=>{
arr.push( item );
})
}
}
fun2(tree);
力扣104 翻转二叉树
树
一种分层数据的抽象模型
简单来说:分层级关系的 。 类似公司组织结构 ,虚拟dom就是树型结构
二叉树 (根节点下永远只有左右2个节点)
eg: 如果通过二叉树遍历下面树的节点?
// 二叉树结构
const tree = {
val:'1',
left:{
val:'2',
left:{val:'4',left:null,right:null},
right:{val:'5',left:null,right:null}
},
right:{
val:'3',
left:{val:'6',left:null,right:null},
right:{val:'7',left:null,right:null}
}
}
前序遍历 即(先序遍历)
核心:根左右
1、递归实现
//递归方式
var preorderTraversal = function(root) {
let arr = [];
var fun = ( node )=>{
if( node ){
//先根节点
arr.push( node.val );
//遍历左子树
fun( node.left );
//遍历右子树
fun( node.right );
}
}
fun( root );
return arr;
};
console.log( preorderTraversal(tree) );
2、非递归版的形式
//非递归版的形式
var preorderTraversal = function(root) {
if( !root ) return [];
let arr = [];
//根节点入栈
let stack = [root];
while( stack.length ){
//出栈
let o = stack.pop();
arr.push( o.val );
o.right && stack.push( o.right );
o.left && stack.push( o.left );
}
return arr;
}
console.log( preorderTraversal(tree) );
中序遍历
核心:左根右 ,先遍历完左节点,在遍历跟节点。
1.递归实现
//递归
var inorderTraversal = function(root) {
const arr = [];
const fun = ( root )=>{
if( !root ) return;
fun( root.left );
arr.push( root.val );
fun( root.right );
}
fun( root );
return arr;
};
console.log( inorderTraversal(tree) );
2、非递归实现
//非递归
<!-- var inorderTraversal = function(root) {
const arr = [];
const stack = [];
let o = root;
while( stack.length || o ){
while( o ){
stack.push( o );
o = o.left;
}
const n = stack.pop();
arr.push( n.val );
o = n.right;
}
return arr;
}
console.log( inorderTraversal(tree) ); -->
后序遍历
核心:左右根 。先遍历完所有左节点,在遍历完右节点
1.递归实现
//递归
var postorderTraversal = function(root) {
const arr = [];
const fun = ( node )=>{
if( node ){
fun( node.left );
fun( node.right );
arr.push( node.val );
}
}
fun( root );
return arr;
};
console.log( postorderTraversal(tree) );
2.非递归实现
//非递归
var postorderTraversal = function(root) {
if( !root ) return [];
let arr = [];
let stack = [root];
while( stack.length ){
const o = stack.pop();
arr.unshift( o.val );
o.left && stack.push( o.left );
o.right && stack.push( o.right );
}
return arr;
}
console.log( postorderTraversal(tree) );
排序(排序啦)
1.冒泡排序
对存放元素的数列,按从前往后的方向进行多次扫描,每次扫描称为一趟。 当发现相邻两个数据顺序错误,将这2个元素进行互换。 相当于比较相邻2个“气泡”的轻重,重的下沉,轻的上浮。 若从小到大排序,较小的数据就会逐个向前移动,好像气泡向上漂浮一样。
个人理解:从前往后依次扫描排序的数列,每次比较相邻2个元素,若是顺序错误,就交换他们的位置。
运行时间: O(n*n) = O(n²)
特点:
eg: 对一组数实现从小到大排序
function arrSort( arr ){
for(let i=0;i<arr.length-1;i++){
for(let j=0;j<arr.length-1-i;j++){
//核心:相邻元素两两对比,若是顺序错误,元素交换
if( arr[j] > arr[j+1]){
let temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
console.log( '第'+(i+1)+'轮,第'+(j+1)+'次交换',arr );
}
}
}
return arr;
}
let arr = [29,10,14,37,14];
console.log( arrSort( arr ) );
冒泡排序优化一
立一个 标志位flag,记录一趟序列遍历中元素是否发生交换,没有交换表示该序列已经有序,可以提前结束了。
function arrSort1( arr ){
for(let i=0;i<arr.length-1;i++){
let isSwap = false;//初始化都是未交换 false:未交换 true:表示交换
for(let j=0;j<arr.length-1-i;j++){
// 交换 设立flag
if( arr[j] > arr[j+1]){
isSwap = true;
let temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
console.log( '---第'+(i+1)+'轮,第'+(j+1)+'次交换',arr );
}
}
if(!isSwap){ //未交换退出循环
break
}
}
return arr;
}
console.log( arrSort1( arr1 ) );
2.选择排序(又叫简单选择排序)
从未排序的元素中找到最大或最小的元素,存放在排序序列的起始位置。 每次从剩余未排序元素中继续寻找最大或最小元素,然后放到已排序序列的末尾. 重复第2步,直到所有元素均排序完毕。
个人理解:每次选出剩余元素中最大的或者最小放在已排序序列的末尾。
特点:
-
需要检查的元素数
越来越少 -
排序中每一轮会
把最大或最小的数移到最前,所以相互比较的次数每一轮都会比前一轮少1次。 依次n, n-1,n-2,n-3,...1; -
选择排序法把N个数通过N-1轮排序。
运行时间: O(n*n) = O(n²)
eg:歌曲排序,播放次数越多的靠前。 每次找到播放次数最多的放在表中。找第一个需n次,找第二个需n-1次,以此直到找到最后一个
function sortMin( arr ){
let indexMin = 0; //最小值的索引
for( let i=0;i<arr.length-1;i++){
indexMin = i; //假定当前索引未最小值的索引
//核心:寻找最小的数,将最小数的索引保存
for( let j=i+1;j<arr.length;j++){
if( arr[j] < arr[indexMin] ){
indexMin = j;
}
}
let temp = arr[i];
arr[i] = arr[indexMin];
arr[indexMin] = temp;
}
return arr;
}
let arr = [29,10,14,37,14];
console.log( sortMin(arr) );
3.插入排序
构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
个人理解:将元素分为已排序的序列 和 未排序的序列2部分。 从当前元素向前比较,扫描已排序的序列,插入的元素与已排序的某个元素相等则插入。
已排序的元素大于(小于)新元素,将元素移动下一个位置。
特点:
- 从第一个元素开始,该元素可以被认为已经被排序
运行时间: O(n*n) = O(n²)
//从小到大排序
function insertionSort(arr) {
var len = arr.length;
var preIndex=0;//已排序的元素索引
var current;//当前要比较的元素cur
for (var i = 1; i < len; i++) {
preIndex = i - 1;
current = arr[i];
//核心:从当前位置,往前比较扫描已排序的序列。比较大小,再移位置,再插入
// 已排序的元素大于新元素,将该元素移动到下一个位置
while(preIndex >= 0 && arr[preIndex] > current) {
arr[preIndex+1] = arr[preIndex];
preIndex--;
}
// 前一个元素(array[j - 1])和后一个元素(array[j])是相同的
// 在下一轮时,当array[j - 1]小于或等于cur 时,将cur 插入array[j](即上一轮的array[j - 1])
arr[preIndex+1] = current;
}
return arr;
}
let arr1 = [5,6,4,2,1,7];
console.log( insertSort(arr1) );
希尔排序(递减增量排序)
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本
理解:将数量按d间隔分组,距离为d的元素放在同一个子序列,在每一个子序列中分别实行直接插入排序。然后缩小间隔d,重复。
步骤: 1.间隔分组 (距离为d的元素放在同一个子序列) 2.组内排序 3.重新设置间隔 4.插入排序
function shellSort(arr) {
var len = arr.length;
let gap = Math.floor(len / 2);//初始化增量
let temp;
console.log(len, gap);
// 每一趟后,需要重新设置间隔
for (gap = gap; gap > 0; gap = Math.floor(gap / 2)) {
console.log('增量', gap);
// 组内,每一趟采用插入排序
for (var i = gap; i < len; i++) {
temp = arr[i];
// 比较大小,交换位置
for (j = i; j >= gap && temp < arr[j - gap]; j -= gap) {
arr[j] = arr[j - gap]
}
arr[j] = temp;
}
}
return arr;
}
let arr = [1, 7, 4, 7, 8, 9, 3, 21, 17, 6, 5]
console.log(shellSort(arr));
4.归并排序
利用归并的思想实现的排序算法,该算法采用经典的 分治法。分治法分为两个阶段。
分阶段:自上而下的递归 。 分解成多个子序列
治阶段:自下而上的迭代 。 合并2个有序子序列
// 归并 - 拆分+合并
let arr = [8, 4, 5, 7, 1, 3, 6, 2];
function mergeSort1(arr) {
// 分阶段:拆分成多个子序列
if (arr.length < 2) return arr;
let mid = Math.floor(arr.length / 2);
let left = arr.slice(0, mid);
let right = arr.slice(mid);
console.log('分-',left, right);
// 合阶段: 合并2个有序的子序列
let merge = function (leftArr, rightArr) {
console.log(leftArr, rightArr);
let resultArr = [];
while (leftArr.length && rightArr.length) {
// shift() 删除数组的第一项元素,返回被删除的元素, 修改原数组
// 若左数组第一个小于等于右边的第一个值,则添加左数组。
// resultArr.push(leftArr[0] <= rightArr[0] ? leftArr.shift() : rightArr.shift())
if(leftArr[0] <= rightArr[0] ){
resultArr.push(leftArr.shift())
}else{
resultArr.push(rightArr.shift())
}
}
// 最后合并
return resultArr.concat(leftArr).concat(rightArr);
}
return merge(
mergeSort1(left),
mergeSort1(right)
);
}
console.log(mergeSort1(arr));
5.快速排序(优于归并排序)
快速排序使用分治法来把一个序列 分为 两个序列。
个人理解: 从数列中选一个基准值做参照,将小的放在基准值前面,将大放在基准值右边。 称作分区操作
注意:
必须设基准值, 可以是数组中任何一个元素 【注设定了基准值后要从原数组删除】
eg: 对数组从小到大排序,以任意一个元素a为基准值,将小于a的所有数放在一起,将大于a的所有数放在一起。最后通过递归调用完成排序
let arr = [29,10,14,37,4,6,12,8,7];
function quickSort( arr ){
if( arr.length <=1 ) return arr;
let mid = Math.floor( arr.length/2 );
let pivot = arr.splice(mid,1)[0]; //设置基准值。
console.log('基准值',pivot);
let left =[]; // 分区
let right = [];
for( let i=0;i<arr.length;i++){
if( arr[i] <pivot ){
left.push( arr[i] );
}else{
right.push( arr[i] );
}
}
console.log('分区',left,right);
// 递归调用
// return quickSort(left).concat([pivot],quickSort(right));
return [...quickSort(left),pivot,...quickSort(right)]
}
console.log( quickSort(arr) );
7.二分查找(折半查找)
是一种在有序数组中查找特定元素的搜索算法。
思想:通过与数组中间元素比较来逐步缩小搜索范围,每次减少一半
大于目标元素,则将指针移动
注意: 必须有序
let arr = [1,5,10,14,20,21,30,36,40];
let target = 30;
// 3个指针分别指向最左边和最右边和中间元素的位置 。
function search( arr , target ){
let start = 0; //起始索引
let end = arr.length-1;//最后索引
let mid = 0; //中间索引
while( start <= end ){
mid = Math.floor( (start+end)/2 ) ;
let guess = arr[mid];//中间值
//如果中间 == 目标值
if( guess== target ){
console.log('猜对了','索引是'+mid);
break;
}
// 目标值大于猜想值,左指针向右移动
if( target > guess ){
start = mid + 1;
console.log('猜小了');
}
// 目标值小于猜想值,右指针向左移动
if( target < guess){
end = mid - 1;
console.log('猜大了');
}
console.log('中间索引,',mid,'假设猜想值:'+mid)
}
}
console.log( search( arr , target) );
堆排序
一、堆是什么?
堆是一种特殊的树,只要满足下面两个条件,它就是一个堆:
(1)堆是一颗完全二叉树,所以又叫二叉堆
(2)堆中某个节点的值总是不大于(或不小于)其父节点的值。
大顶堆:每个节点的值都大于或等于其子节点的值。 在堆排序算法中用于升序排列;
小顶堆:每个节点的值都小于或等于其子节点的值。 在堆排序算法中用于降序排列;
堆都能用树来表示,一般树的实现都是利用链表。
但堆是通过数组来实现的(不是通过链表)
二叉堆易于存储,并且便于索引
二、在堆的实现时,需要注意:
因为是数组,所以父子节点的关系就不需要特殊的结构去维护了,索引之间通过计算就可以得到,省掉了很多麻烦。如果是链表结构,就会复杂很多;
完全二叉树要求叶子节点从左往右填满,才能开始填充下一层,这就保证了不需要对数组整体进行大片的移动。这也是随机存储结构(数组)的短板:删除一个元素之后,整体往前移是比较费时的。这个特性也导致堆在删除元素的时候,要把最后一个叶子节点补充到树根节点的缘由
堆是完全二叉树,所以堆的索引与数组下标一一对应
二叉堆在数组里,通过当前下标怎么就能找到父节点和子节点呢?
通过Math.floor取索引为整数
- 左孩子索引:
2 * index + 1 - 右孩子索引:
2 * index + 2 - 父节点索引:
( index - 1 ) / 2
堆排序
步骤: 1、 构建最大堆
操作:从最后一个非叶子节点处理,与其孩子节点比较,与其较大的孩子节点 交换位置,只到比较完所有非叶子节点。(上浮)
2、 排序更新堆
操作: (1)、最大堆构建好后,根节点与最后一个叶子节点交换。
(2)、交换后更新最大堆的情况(下沉)
(3)、重复(1)、(2)
var len; // 因为声明的多个函数都需要数据长度,所以把len设置成为全局变量
function buildMaxHeap(arr) { // 建立大顶堆
len = arr.length;
for (var i = Math.floor(len/2); i >= 0; i--) {
heapify(arr, i);
}
}
function heapify(arr, i) { // 堆调整
var left = 2 * i + 1,
right = 2 * i + 2,
largest = i;
if (left < len && arr[left] > arr[largest]) {
largest = left;
}
if (right < len && arr[right] > arr[largest]) {
largest = right;
}
if (largest != i) {
swap(arr, i, largest);
heapify(arr, largest);
}
}
function swap(arr, i, j) {
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
function heapSort(arr) {
buildMaxHeap(arr);
for (var i = arr.length-1; i > 0; i--) {
swap(arr, 0, i);
len--;
heapify(arr, 0);
}
return arr;
}
var arr = [1,4,68,20,6,3,9];
console.log(heapSort(arr));