前端面试题集锦——算法

1 篇文章 0 订阅
1 篇文章 0 订阅

前端面试题集锦——算法

时间复杂度

时间复杂度(Time complexity)是一个函数,它定性描述该算法的运行时间。这是一个代表算法输入值的字符串的长度的函数. 时间复杂度常用大O表述,不包括这个函数的低阶项和首项系数。

  • 找到执行次数最多的语句 (语句频度最高的)
  • 计算语句执行次数的数量级
  • 用大O来表示结果

空间复杂度

是什么?

执行当前算法需要占用多少内存空间

表示法

O(1)、O(n)、O(n^2)…

空间复杂度基本上是O(1)或者O(N),其它的空间复杂度不常见。假设开一个N*N的数组,那么它的空间复杂度是O(N^2)。结构体不讨论结构体个数,只看整体。不看具体,只看量级。

线性表

链表

链表是由多个元素组成的列表,链表中的元素储存不连续,用next指针连接在一起。

数组:增删非数组元素需要移动元素。

链表:增删非首尾元素不需要移动元素只需要更改next的指向即可

链表是一个链式数据结构,每个节点由两个信息组成:节点的数据和指向下一个节点的指针。链表和传统数组都是线性数据结构,具有序列化的存储方式。

操作数组链表
内存分配编译和序列化过程中静态分配运行过程中动态分配
获取元素从索引中读取读取队列中的所有节点,直到特定元素,较慢
增加/删除元素顺序增加删除,较慢动态分配,内存消耗小,速度快
空间结构一维或者多维单边/多边,循环链表

JavaScript中没有链表,但是可以用object来模拟链表

const a = { val: 'a' };
const b = { val: 'b' };
const c = { val: 'c' };
const d = { val: 'd' };
// a的next属性指向b
a.next = b;
b.next = c;
c.next = d;
d.next = null;

console.log( a );
// 这个嵌套的object就相当于一个链表

遍历链表
遍历链表就是跟着链表从链表的头元素(head)一直走到尾元素(但是不包含链表的头节点,头通常用来作为链表的接入点)还有一个问题,链表的尾元素指向一个null节点
// 声明一个指针,指向a
let p = a;
    // 当p还有值得时候
while (p && p.val) {
    console.log(p.val);
        // 不断得让p指向下一个位置
    p = p.next;
}

插入链表
// 在链表中插入值
const f = { val: 'f' };
c.next = f;
f.next = d;

删除链表
// 删除值
c.next = d
1.instanceof原理
// let arr = [1,2,3];
// console.log( arr instanceof Object );
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 ) );
2.环形链表

给你一个链表的头节点 head ,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。

如果链表中存在环 ,则返回 true 。 否则,返回 false 。

var hasCycle = function(head) {
    let f = head,s = head;
    while( f != null && f.next != null ){
        s = s.next;
        f = f.next.next;
        if( s == f){
            return true;
        }
    }
    return false;
}
2.删除链表中的节点

有一个单链表的 head,我们想删除它其中的一个节点 node。

给你一个需要删除的节点 node 。你将无法访问 第一个节点 head。

链表的所有值都是 唯一的,并且保证给定的节点 node 不是链表中的最后一个节点。

删除给定的节点。注意,删除节点并不是指从内存中删除它。这里的意思是:

  • 给定节点的值不应该存在于链表中。
  • 链表中的节点数应该减少 1。
  • node 前面的所有值顺序相同。
  • node 后面的所有值顺序相同。
var deleteNode = function(node) {
    node.val = node.next.val;
	node.next = node.next.next;
};
4.删除排序链表中的重复元素

给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回已排序的链表 。

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;
};
5.反转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

var reverseList = function(head) {
    let prev = null;
    let curr = head;
    while (curr) {
        const next = curr.next;
        curr.next = prev;
        prev = curr;
        curr = next;
    }
    return prev;
};

栈作为一种数据结构,是一种只能在一端进行插入和删除操作的特殊线性表。它按照先进后出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(最后一个数据被第一个读出来)。

JavaScript中没有栈,但是可以用Array实现栈的功能。

栈中数组长度减一即为栈尾元素,也就是最后进入的那个元素,最先出去的那个元素。

JavaScript中对栈的操作一般会使用到

push()方法,将元素压入栈顶
pop()方法,从栈顶弹出(删除)元素,并返回该元素
peek()方法,返回栈顶元素,不删除
clear()方法,清空栈
length拿到栈中元素数量
1.删除字符串中的所有相邻重复项

给出由小写字母组成的字符串 S,重复项删除操作会选择两个相邻且相同的字母,并删除它们。

在 S 上反复执行重复项删除操作,直到无法继续删除。

在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。

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') );//ca
2.有效的括号

给定一个只包括 ‘(’,‘)’,‘{’,‘}’,‘[’,‘]’ 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

  • 左括号必须用相同类型的右括号闭合。
  • 左括号必须以正确的顺序闭合。
  • 每个右括号都有一个对应的相同类型的左括号。
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("()"));//true
console.log( isValid("()[]{}"));//true
console.log( isValid("(]"));//false
3.简化路径

给你一个字符串 path ,表示指向某一文件或目录的 Unix 风格 绝对路径 (以 ‘/’ 开头),请你将其转化为更加简洁的规范路径。

在 Unix 风格的文件系统中,一个点(.)表示当前目录本身;此外,两个点 (…) 表示将目录切换到上一级(指向父目录);两者都可以是复杂相对路径的组成部分。任意多个连续的斜杠(即,‘//’)都被视为单个斜杠 ‘/’ 。 对于此问题,任何其他格式的点(例如,‘…’)均被视为文件/目录名称。

请注意,返回的 规范路径 必须遵循下述格式:

  • 始终以斜杠 ‘/’ 开头。
  • 两个目录名之间必须只有一个斜杠 ‘/’ 。
  • 最后一个目录名(如果存在)不能 以 ‘/’ 结尾。
  • 此外,路径仅包含从根目录到目标文件或目录的路径上的目录(即,不含 ‘.’ 或 ‘…’)。

返回简化后得到的 规范路径 。

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/') );
console.log( simplifyPath('/../') );
console.log( simplifyPath('/home//foo/') );
console.log( simplifyPath('/a/./b/../../c/') );

集合

一种无序且唯一的数据结构。

ES6中有集合 Set类型

字典

与集合类似,一个存储唯一值的结构,以键值对的形式存储。类似于js的对象(键[key]都是字符串类型或者会转换成字符串类型)。

js中有字典数据结构 就是 Map 类型,map的键不会转换类型。

var a = {}
var b = {
	key:'a'
}	
var c = {
	key:'c'
}
a[b] = '123';   
a[c] = '456';	
console.log( a[b] );//456

let map = new Map();
map.set('a','1');
map.set('b','2');
console.log( map );
console.log( map.get('b') );
console.log( map.has('x') );

map.delete('a');
console.log( map );

var a = new Map();
var b = {
	key:'a'
}	
var c = {
	key:'c'
}
a.set(b,'123');
a.set(c,'456');
console.log( a , a.get(b) , a.get(c) );

哈希表

又叫 散列表 在js中没有哈希表,哈希表是字典一种实现。

区别一:如果找key对应的value需要遍历key,那么想要省去遍历的过程,用哈希表来表示。

区别二:排列顺序

  • 字典是根据添加的顺序进行排列的
  • 哈希表不是添加的顺序进行排列的
class HashTable{
	constructor(){
		this.table = [];
	}
	hashCode( key ){
		let hash = 0;
		for( let i =0;i<key.length;i++){
			hash += key.charCodeAt(i);
		}
		return hash;
	}	
	put( key , val ){
		let hashKey = this.hashCode(key);
		this.table[ hashKey ] = val;
	}
	get( key ){
		let hashKey = this.hashCode(key);
		return this.table[hashKey];
	}
}
let hashTable = new HashTable();
hashTable.put('person','章三');
console.log( hashTable );
console.log(  hashTable.get('person') );
1.两数之和

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

你可以按任意顺序返回答案。

var twoSum = function(nums, target) {
    let map = new Map();
    for( let i=0;i<nums.length;i++){
        num = target - nums[i];
        if( map.has(num) ){
            return [map.get(num),i];
        }
        map.set(nums[i],i);
    }
};
2.存在重复元素

给你一个整数数组 nums 。如果任一值在数组中出现 至少两次 ,返回 true ;如果数组中每个元素互不相同,返回 false 。

var containsDuplicate = function(nums) {
    let set = new Set();
    for( const x of nums ){
        if( set.has(x) ){
            return true;
        }
        set.add(x);
    }
    return false;
};
3.两个数组的交集

给定两个数组 nums1 和 nums2 ,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。

var intersection = function(nums1, nums2) {
    let set = new Set(nums2);
    return [...new Set(nums1)].filter((val)=>set.has(val));
};
//filter()方法用于对数组进行过滤。创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素。

4.判断一个字符串中出现次数最多的字符,并返回次数

function fun( s ){
	let maxNum = 0;
	let maxStr = '';
	let map = new Map();
	for( let item of s ){
		map.set( item , (map.get(item) || 0 ) + 1 )
	}
	for(let [key,value] of map){
		if( value > maxNum ){
			maxStr = key;
			maxNum = value;
		}
	}
	return [maxStr , maxNum];
}
console.log( fun('aaabbbbccccccc') );
5.独一无二的出现次数

给你一个整数数组 arr,请你帮忙统计数组中每个数的出现次数。

如果每个数的出现次数都是独一无二的,就返回 true;否则返回 false。

var uniqueOccurrences = function(arr) {
	const map = new Map();
    for (const x of arr) {
        if (map.has(x)) {
            map.set(x, map.get(x) + 1);
        } else {
            map.set(x, 1);
        }
    }
    const set = new Set();
    for (const [key, value] of map) {
        set.add(value);
    }
    return set.size === map.size;
};
6…无重复字符的最长子串

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

var lengthOfLongestSubstring = function(s) {
    const map = new Map();
    let l = 0;
    let num = 0;
    for( let i =0;i<s.length;i++){
        if( map.has(s[i]) && map.get(s[i]) >= l ){
            l = map.get(s[i]) + 1;
        }
        num = Math.max( num , i+1-l );
        map.set(s[i],i);
    }
    return num;
};

队列

JavaScript中没有队列这个数据结构,但是可以用数组来实现所有的功能。

队列是一个先进先出的数据结构,一般JavaScript中采用队列解决问题时会用到

1. 入队push ():在数组的尾部添加元素
2. 出队shift ():移除数组中第一个元素
3. queue (0) :取数组的第一个元素
4. isEmpty ():确定队列是否为空
5. size ():获取队列中元素的数量

1.最近的请求次数

写一个 RecentCounter 类来计算特定时间范围内最近的请求。

请你实现 RecentCounter 类:

  • RecentCounter() 初始化计数器,请求数为 0 。
  • int ping(int t) 在时间 t 添加一个新请求,其中 t 表示以毫秒为单位的某个时间,并返回过去 3000 毫秒内发生的所有请求数(包括新请求)。确切地说,返回在 [t-3000, t] 内发生的请求数。

保证每次对 ping 的调用都使用比之前更大的 t 值。

var RecentCounter = function() {
    this.queue  = [];
};
RecentCounter.prototype.ping = function(t) {
    this.queue .push( t );
    while( this.queue[0] < t-3000 ){
        this.queue .shift();
    }
    return this.queue .length;
};
var obj = new RecentCounter();
var param_1 = obj.ping(t);

前端树结构还是比较常见的,例如级联选择、层级目录等都是树形结构。

javascript中没有树这个数据结构,但是一般用object和arrey来模拟树。

const tree = {
	values:a,
	children:[
        {
            values:b,
            children:[
                {
                    values:d,
                    children:[]
                },
                {
                    values:e,
                    children:[]
                },{
                 	values:f,
                    children:[]
                }
            ]
        },
        {
            values:c,
            children:[
                {
                    values:g,
                    children:[]
                },
                {
                    values:h,
                    children:[]
                }
            ]
        }
    ]
}

树的常用遍历方式

深度优先遍历
尽可能深的遍历树的分支。先访问根节点,然后再对子节点挨个使用深度优先遍历。

const deepNood = (root)=>{
    console.log(root.values);//打印根节点
    root.children.forEach(child => {//遍历子节点
        deepNood(child)//递归
    })
}
deepNood(tree);//a b d e c f g 

广度优先遍历

优先访问距离根节点最近的节点。广度优先遍历需要使用到队列这个数据结构

  • 新建一个队列,把根节点入队
  • 将队头出队并访问
  • 将队头的children顺序入队
  • 重复第二步和第三步,直到队列为空
const breadth = (root)=>{
    const arr = [root];//将树加入队列(整个object对象,)
    while(arr.length > 0){	//队列是否为空
        const val = arr.shift() //从队列中取出根节点
        console.log(val.values)
        for(let child of val.children){ //遍历子节点
            arr.push(child) //将子节点加入队列
        }
// 打开console.log(arr),就能看出不断的将childern入队,然后再将队头取出
    }
}
breadth(tree);//a b c d e f g 
二叉树

二叉树是n(n>=0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树组成。

二叉树的每个节点最多只能有两个子节点。

js中自然也没有二叉树这个数据结构,一般还是用object对象来模拟二叉树。

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
                }
        }
}

二叉树遍历(递归)

前序遍历

DLR:根节点——左子树——右子树

每次遍历到一个节点都重复一次前序遍历

//递归方式
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) );

//非递归版的形式
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) );

中序遍历

LDR:左子树——根节点——右子树

每次遍历到一个节点都重复一次中序遍历

//递归
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) );

//非递归
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) );

后序遍历

LRD:左子树——右子树——根节点

每次遍历到一个节点都重复一次后序遍历

//递归
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) );

//非递归
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) );

完全二叉树

满二叉树

深度为 h, 有 n 个节点,且满足 n = 2^h - 1

二叉查找树: 是一种特殊的二叉树,能有效地提高查找效率

  • 小值在左,大值在右

  • 节点 n 的所有左子树值小于 n,所有右子树值大于 n

插入与删除节点

1.二叉树的最小深度

给定一个二叉树,找出其最小深度。

最小深度是从根节点到最近叶子节点的最短路径上的节点数量。

说明:叶子节点是指没有子节点的节点。

var minDepth = function(root) {
    if( !root ) return 0;
	const stack = [ [root,1] ];
	while(  stack.length ){
		const [o,n] = stack.shift();
		if( !o.left && !o.right ){
			return n;
		}
        if( o.left ) stack.push(  [o.left,n+1] );
		if( o.right ) stack.push(  [o.right,n+1] );
	}
};
2.二叉树的最大深度

给定一个二叉树,找出其最大深度。

二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。

说明: 叶子节点是指没有子节点的节点。

var maxDepth = function(root) {
	if( !root ) return 0;
	const stack = [root];
	let num = 0;
	while( stack.length ){
		let len = stack.length;
		num++;
		while( len-- ){
			const o = stack.shift();
			o.left && stack.push(  o.left );
			o.right && stack.push(  o.right );
		}
	}
	return num;
};
console.log( maxDepth(tree) );
3.翻转二叉树

给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。

var invertTree = function(root) {
	if(  root == null ) return null;
	let tmp = root.left;
	root.left = root.right;
	root.right = tmp;
	invertTree(root.left);
	invertTree(root.right);
	return root;
};
console.log( invertTree(tree) );
4.相同的树

给你两棵二叉树的根节点 p 和 q ,编写一个函数来检验这两棵树是否相同。

如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。

var isSameTree = function(p, q) {
	if( p === null && q === null ) return true;
	if( p === null || q === null ) return false;
	if( p.val !== q.val ) return false;
	return isSameTree( p.left , q.left ) && isSameTree( p.right , q.right );
};
console.log( isSameTree(  tree1,tree2  )  );

如果一棵树可以拥有多个父节点,那么它就变成了一张图(Graph)。在图中连接每个节点的边,可以是有方向的,也可以是无方向的,可以是有权重的,也可以是无权重的。既有方向也有权重的边类似于向量。

社交网络和互联网本身也是图。自然界中最复杂的图是我们人类的大脑。现在,我们试图将神经网络复制到机器中,期望使机器具有「超级智能」。

堆,一般由操作人员(程序员)分配释放,若操作人员不分配释放,将由OS(操作系统)回收释放。分配方式类似链表。堆存储在二级缓存中。

堆都能用树来表示,并且一般树的实现都是利用链表。

而二叉堆是一种特殊的堆,它用完全二叉树表示,却可以利用数组实现。平时使用最多的是二叉堆,它可以用完全二叉树表示,二叉堆易于存储,并且便于索引。

堆数据结构像树,但是,是通过数组来实现的(不是通过链表:二叉堆)

在堆的实现时,需要注意:

因为是数组,所以父子节点的关系就不需要特殊的结构去维护了,索引之间通过计算就可以得到,省掉了很多麻烦。如果是链表结构,就会复杂很多;

完全二叉树要求叶子节点从左往右填满,才能开始填充下一层,这就保证了不需要对数组整体进行大片的移动。这也是随机存储结构(数组)的短板:删除一个元素之后,整体往前移是比较费时的。这个特性也导致堆在删除元素的时候,要把最后一个叶子节点补充到树根节点的缘由。

二叉堆想树的样子我可以理解,但将它们安排在数组里的话,通过当前下标怎么就能找到父节点和子节点呢?

左: 2 * index + 1
右: 2 * index + 2
找父: ( index - 1 ) / 2

1.最小堆
class MinHeap{
	constructor(){
		this.heap = [];
	}
	//换位置
	swap(i1,i2){
		const temp = this.heap[i1];
		this.heap[i1] = this.heap[i2];
		this.heap[i2] = temp;
	}
	//找到父节点
	getParentIndex( index ){
		return Math.floor( (index-1)/2  );
	}
	//上(前)移操作
	up( index ){
		//如果是0就不移动了
		if( index ==0 )return;
		const parentIndex = this.getParentIndex( index );
		//如果父元素大于当前元素,就开始移动
		if( this.heap[parentIndex] > this.heap[index] ){
			this.swap(  parentIndex  ,  index );
			this.up( parentIndex );
		}
	} 
	//获取左侧子节点
	getLeftIndex( index ){
		return index * 2 + 1;
	}
	//获取右侧子节点
	getRightIndex( index ){
		return index * 2 + 2;
	}
	//下(后)移操作
	down( index ){
		const leftIndex = this.getLeftIndex( index );
		const rightIndex = this.getRightIndex( index );
		if( this.heap[leftIndex] < this.heap[index] ){
			this.swap(leftIndex,index);
			this.down( leftIndex );
		}
		if( this.heap[rightIndex] < this.heap[index] ){
			this.swap(rightIndex,index);
			this.down( rightIndex );
		}
	}
	//添加元素
	insert( value ){
		this.heap.push( value );
		this.up( this.heap.length-1 );
	}
	//删除堆顶
	pop(){
		this.heap[0] = this.heap.pop();
		this.down( 0 );
	}
	//获取堆顶
	peek(){
		return this.heap[0]
	}
	size(){
		return this.heap.length;
	}
}
let arr = new MinHeap();
arr.insert( 5 );
arr.insert( 4 );
arr.insert( 6 );
arr.insert( 1 );
arr.pop();
console.log( arr );
console.log( arr.size() );
console.log( arr.peek() );
2.数组中的第K个最大元素

给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。

请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。

var findKthLargest = function(nums, k) {
    let arr = new MinHeap();
    nums.forEach(item=>{
        arr.insert( item );
        if( arr.size() > k ){
            arr.pop();
        }
    })
    return arr.peek();
};
findKthLargest([3,2,1,5,6,4],2);

查找

二分搜索
let arr = [1,2,3,4,5,6,7];
let target = 6;
function search( arr , target ){
	let conut = 1;
	let start = 0;
	let end = arr.length-1;
	while(  start<=end  ){
		//取出中间值
		let middle = Math.floor( (start+end)/2 );
		let guess = arr[middle];
		//如果中间 == 目标值
		console.log( conut );
		if( guess== target ){
			return middle;//返回位置
		}
		if( guess > target ){
			end = middle;
		}
		if( guess < target ){
			start = middle + 1;
		}
		conut++;
	}
	return -1;
}
console.log( search( arr , target) );

排序

冒泡排序

两两比较

冒泡排序的原理:

每一趟只能确定将一个数归位。即第一趟只能确定将末位上的数归位,第二趟只能将倒数第 2 位上的数归位,依次类推下去。如果有 n 个数进行排序,只需将 n-1 个数归位,也就是要进行 n-1 趟操作。而 “每一趟 ” 都需要从第一位开始进行相邻的两个数的比较,将较大的数放后面,比较完毕之后向后挪一位继续比较下面两个相邻的两个数大小关系,重复此步骤,直到最后一个还没归位的数。

function bubbleSort( 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;
			}
		}
	}
	return arr;
}
let arr = [29,10,14,37,14];
console.log( bubbleSort( arr ) );

冒泡排序总的平均时间复杂度为O(n^2)。

选择排序

第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,继续放在起始位置知道未排序元素个数为0。

遍历自身以后的元素,最小的元素跟自己调换位置

  • 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
  • 再从剩余未排序元素中继续寻找最小(大)元素,然后放到未排序序列的起始位置。
  • 重复第二步,直到所有元素均排序完毕。
function selectSort( 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( selectSort(arr) );

选择排序的时间复杂度为O(n^2)。

插入排序

即将元素插入到已排序好的数组中

  1. 从第一个元素开始,该元素可以被认为已经被排序
  2. 取出下一个元素,在已经排序号的序列中,从后往前扫描
  3. 直到找到小于或者等于该元素的位置
  4. 将该位置后面的所有已经排序的元素从后往前移动
  5. 将该元素插入到该位置
  6. 重复步骤(2-5)
function insertSort( arr ){
	let len = arr.length;
	for(let i=1;i<len;i++){
		let temp = arr[i];
		let j = i-1;//默认已排序的元素
		//在已经排序好的队列进行从后到前的扫描
		while( j>=0 && arr[j] > temp ){
			//已排序的元素大于新元素,将该元素移动到下一个位置
			arr[j+1] = arr[j];
			j--;
		}
		arr[j+1] = temp;
	}	
	return arr;
}
let arr = [5,3,4,2,1];
console.log(  insertSort(arr)  );

插入排序最坏情况下的时间复杂度为O(n^2)。

归并排序

归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法,归并排序对序列的元素进行逐层折半分组,然后从最小分组开始比较排序,合并成一个大的分组,逐层进行,最终所有的元素都是有序的。

let arr = [8,4,5,7,1,3,6,2];
function mergeSort( arr ){
	if( arr.length < 2 ) return arr;
	let mid = Math.floor(  arr.length/2 );
	let merge = function(leftArr,rightArr){
		console.log( leftArr,rightArr );
		let resultArr = [];
		while( leftArr.length && rightArr.length ){
			resultArr.push(  leftArr[0] <= rightArr[0] ? leftArr.shift() : rightArr.shift()   )
		}
		return resultArr.concat(leftArr).concat(rightArr);
	}
	return merge( 
		mergeSort(arr.slice(0,mid)),
		mergeSort(arr.slice(mid))
	);
}
console.log( mergeSort(arr)  );
快速排序

每趟排序时选出一个基准值,然后将所有元素与该基准值比较,并按大小分成左右两堆,然后递归执行该过程,直到所有元素都完成排序。

  • 先从数列中取出一个数作为基准数。
  • 分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
  • 再对左右区间重复第二步,直到各区间只有一个数。

选择基准值(base),原数组长度减一(基准值),使用 splice

循环原数组,小的放左边(left 数组),大的放右边(right 数组);

concat(left, base, right) 递归继续排序 left 与 right

let arr = [29,10,14,37,4];
function quickSort( arr ){
	if( arr.length <=1 ) return arr;
	let mid = Math.floor(  arr.length/2  );
	let pivot = arr.splice(mid,1)[0];
	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] );
		}
	}
	return quickSort(left).concat([pivot],quickSort(right));
}
console.log( quickSort(arr)  );
希尔排序

不定步数的插入排序,插入排序

口诀: 插冒归基稳定,快选堆希不稳定

稳定性: 同大小情况下是否可能会被交换位置, 虚拟 dom 的 diff,不稳定性会

导致重新渲染;

递归运用

(斐波那契数列): 爬楼梯问题

初始在第一级,到第一级有 1 种方法(s(1) = 1),到第二级也只有一种方法(s(2) = 1),

第三级(s(3) = s(1) + s(2))

function cStairs(n) {

if(n === 1 || n === 2) {

return 1;

} else {

return cStairs(n-1) + cStairs(n-2)

}

}

五大算法

贪心算法

局部最优解法

分治算法

分成多个小模块,与原问题性质相同

动态规划

每个状态都是过去历史的一个总结

回溯法

发现原先选择不优时,退回重新选择

分支限界法

12.验证一个数是否是素数

素数也叫质数,即最大约数是自己本身的自然数。也就是说,比0大,一个大于1的自然数,只能整除1和自己本身;否则称为合数。

  • 如果这个数是 2 或 3,一定是素数;
  • 如果是偶数,一定不是素数;
  • 如果这个数不能被3~它的平方根中的任一数整除,m必定是素数。而且除数可以每次递增2(排除偶数)
function isPrime(num){
	if (num === 2 || num === 3) {
		return true;
	};
	if (num % 2 === 0) {
		return false;
	};
	let divisor = 3,limit = Math.sqrt(num);
    //Math.sqrt(x) -- 返回数字的平方根 
	while(limit >= divisor){
		if (num % divisor === 0) {
			return false;
		}
		else {
			divisor += 2;
		}
	}
	return true;
}
console.log(isPrime(30));  // false

13.斐波那契

斐波那契数列(Fibonacci sequence),又称黄金分割数列,因数学家莱昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”。

斐波那契数列指的是这样一个数列:

0,1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181,6765,10946,17711……

它的规律是:这个数列从第 3 项开始,每一项都等于前两项之和。

在数学上,斐波那契数列以如下被以递推的方法定义:F(0)=0,F(1)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 2,n ∈ N*),显然,斐波那契数列是一个线性递推数列。

最简单的做法:递归。

function fibonacci(n) {
	if (n == 0) {
		return 0;
	}else if (n == 1 || n == 2) {
		return 1;
	}else {
		return fibonacci(n - 1) + fibonacci(n - 2);
	}            
}

但是递归会有严重的效率问题。比如想要求得f(10),首先需要求f(9)和f(8)。同样,想求f(9),首先需要f(8)和f(7)…这样就有很多重复值,计算量也很大。

改进:从下往上计算,首先根据f(0)和f(1)计算出f(2),再根据f(1)和f(2)计算出f(3)……以此类推就可以计算出第n项。时间复杂度O(n)function fibonacci(n){
	let ori = [0,1];
	if (n < 2) {
		return ori[n];
	};
	let fiboOne = 0,fiboTwo = 1,fiboSum = 0;
	for (let i = 2; i < n; i++) {
		fiboSum = fiboOne + fiboTwo;
		fiboOne = fiboTwo;
		fiboTwo = fiboSum;
	}
	return fiboSum;
}
console.log(fibonacci(5));// 3

14.求最大公约数

指两个或多个整数共有约数中最大的一个。

约数:又称因数。整数a除以整数b(b≠0) 除得的商正好是整数而没有余数,我们就说a能被b整除,或b能整除a。a称为b的倍数,b称为a的约数。

除数 在a和b的范围内,如果同时a和b处以除数的余等于0,就将此时的除数赋值给res;除数自增,不断循环上面的计算,更新res。

解法1function greatestCommonDivisor(a, b){
	let divisor = 2,res = 1;
	if (a < 2 || b < 2) {
		return 1;
	};
	while(a >= divisor && b >= divisor){
		if (a % divisor === 0 && b % divisor === 0) {
			res = divisor;
		}
		divisor++;
	}
	return res;
};
console.log(greatestCommonDivisor(8, 4)); // 4
console.log(greatestCommonDivisor(69, 169)); // 1

解法2:
辗转相除法 也叫欧几里德算法。以除数和余数反复做除法运算,当余数为 0 时,取当前算式除数为最大公约数。
1.先用 x 除以 y
2.若余数为 0 则 y 为两数的最大公约数;若余数不为零,则令 x = y,y = 余数,重复步骤 1 直到余数为 0,此时的 y 为两数的最大公约数。
function greatestCommonDivisor(a,b){
	if (b % a === 0) {
		return a;
	} else {
		return greatestCommonDivisor(b,a % b);
	}
}

(更损相减法)
1、任意给定两个正整数;判断它们是否都是偶数。若是,则用2约简;若不是则执行第2步。
2、以较大的数减较小的数,接着把所得的差与较小的数比较,并以大数减小数。继续这个操作,直到所得的减数和差相等为止。
3、第2步中约掉的若干个2与第2步中等数的乘积就是所求的最大公约数。

function maxcommonDivisor(n1, n2) {
    n1 = n1 && parseInt(n1);
    n2 = n2 && parseInt(n2);
    // 声明变量sum记录整除2的次数
    let sum = 1;
    // 使用while循环 对两个整数用2分别求余
    while(n1 % 2 == 0 && n2 % 2 == 0) {
        n1 = n1 / 2;
        n2 = n2 / 2;
        sum *= 2;
    }
    // 比较n1,n2的大小,使用解构赋值,使较大的数等于n1
    if(n1 < n2) {
        [n1, n2] = [n2, n1];
    }
    // 较大的数减较小的数,求差
    let poor = n1 - n2;
    // 使用while循环判断 差 是否等于n2, 如果不等于,则继续使较大数减较小数求差
    while(poor != n2) {
        if(poor < n2) {
            [n1, n2] = [n2, poor];
        } else {
            [n1, n2] = [poor, n2];
        }
        poor = n1 - n2;
    }
    // 若干个2 乘以 得出的差,就是最大公约数
    return poor * sum;
}

15.数组去重

对原数组进行遍历
	获取arr[i]的值 j;
	对应到辅助数组 exits 的位置 j 的值,如果没有,则证明arr[i] 的值没有重复,此时将值j存入res数组,并将辅助数组j位置的值置为 true。
最后返回res数组。

function removeDuplicate(arr){
	if (arr === null || arr.length < 2) {
		return arr;
	};
	let res = [],exits = [];
	for(let i = 0; i < arr.length; i++){
		let j = arr[i];
		while( !exits[j] ){
			res.push(arr[i]);
			exits[j] = true;
		}
	}
	return res;
}
console.log(removeDuplicate([1,3,3,3,1,5,6,7,8,1]))  
// [1,3,5,6,7,8]

16.删除重复的字符

这一题的解法和上一题类似。

function removeDuplicateChar(str){
	if (!str || str.length < 2 || typeof str != "string") {
		return;
	};
	let charArr = [],res = [];
	for(let i = 0; i < str.length; i++){
		let c = str[i];
		if(charArr[c]){
			charArr[c]++;
		}
		else{
			charArr[c] = 1;
		}
	}
	for(let j in charArr){
		if (charArr[j] === 1) {
			res.push(j);
		}
	}
	return res.join("");
}
console.log(removeDuplicateChar("Learn more javascript dude"));
// Lnmojvsciptu

17.排序两个已经排好序的数组

如果 b数组已经遍历完,a数组还有值 或 a[i] 的值 小于等于 b[i] 的值,则将 a[i] 添加进数组res,并 i++;
如果不是上面的情况,则将 b[i] 添加进数组res,并 i++function mergeSortedArr(a,b){
	if (!a || !b) {
		return;
	};
	let aEle = a[0],bEle = b[0],i = 1,j = 1,res = [];
	while(aEle || bEle){
		if ((aEle && !bEle) || aEle <= bEle) {
			res.push(aEle);
			aEle = a[i++];
		}
		else{
			res.push(bEle);
			bEle = b[j++];
		}
	}
	return res;
}
console.log(mergeSortedArr([2,5,6,9], [1,2,3,29]))  // [1,2,2,3,5,6,9,29]

18.字符串反向

最简单的方法:

function reverse(str){
	let resStr = "";
	for(let i = str.length-1; i >= 0; i--){
		resStr += str[i];
	}
	return resStr;
}
console.log(reverse("ABCDEFG"));

方法2

function reverse2(str){
	if (!str || str.length < 2 || typeof str != "string") {
		return str;
	};
	let res = [];
	for(let i = str.length-1; i >= 0; i--){
		res.push(str[i]);
	}
	return res.join("");
}
console.log(reverse2("Hello"));

将函数添加到String.prototype

String.prototype.reverse3 = function(){
	if (!this || this.length < 2) {
		return;
	};
	let res = [];
	for(let i = this.length-1; i >= 0; i--){
		res.push(this[i]);
	}
	return res.join("");
}
console.log("abcdefg".reverse3());

19.字符串原位反转

例如:将“I am the good boy”反转变为 “I ma eht doog yob”。

提示:使用数组和字符串方法。

function reverseInPlace(str){
	return str.split(' ').reverse().join(' ').split('').reverse().join('');
//split():把字符串分割成字符串数组(split()不改变原始字符串)
//reverse():用于颠倒数组中元素的顺序
//join():用于把数组中所有元素转换连接成一个字符串
}
console.log(reverseInPlace('I am the good boy'));
//I ma eht doog yob

20.判断是否是回文

palindrome(回文)是指一个字符串忽略标点符号、大小写和空格,正着读和反着读一模一样。

function isPalindrome(str){
	if (!str || str.length < 2) {
		return;
	}
	for(let i = 0; i < str.length/2; i++){
		if (str[i] !== str[str.length-1-i]) {
			return false;
		}
	}
	return true;
}
console.log(isPalindrome("madama"))//false
console.log(isPalindrome("amama"))//true

21.判断数组中是否有两数之和

eg:在一个未排序的数组中找出是否有任意两数之和等于给定的数。

给出一个数组[6,4,3,2,1,7]和一个数9,判断数组里是否有任意两数之和为9。

循环遍历数组,let subStract = num - arr[i];
如果 differ[subStract] 里有值,则返回true;如果没有,将 differ[arr[i]] 置为 truefunction sumFind(arr,num){
	if (!arr || arr.length < 2) {
		return;
	};
	let differ = {};
	for(let i = 0; i < arr.length; i++){
		let subStract = num - arr[i];
		if (differ[subStract]) {
			return true;
		}
		else{
			differ[arr[i]] = true;
		}
	}
	return false;
}
console.log(sumFind([6,4,3,2,1,7], 9));  // true

22.连字符转成驼峰

如:get-element-by-id 转为 getElementById

let str = 'get-element-by-id';
let arr = str.split('-');
//split():把字符串分割成字符串数组(split()不改变原始字符串)
for(let i=1; i<arr.length; i++){
  arr[i] = arr[i].charAt(0).toUpperCase() + arr[i].substring(1);
//charAt() 方法可返回指定位置的字符。
//toUpperCase()返回一个字符串,该字符串中的所有字母都被转化为大写字母。
//substring() 方法用于提取字符串中介于两个指定下标之间的字符。
}
console.log(arr.join(''));   // getElementById

23.最长公共前缀

编写一个函数来查找字符串数组中的最长公共前缀。

如果不存在公共前缀,返回空字符串 “”。

示例 1:

输入: [“flower”,“flow”,“flight”]
输出: “fl”

示例 2:

输入: [“dog”,“racecar”,“car”]
输出: “”

function longestCommonPrefix(arr) {
    if(arr.length === 0){  // 简单逻辑判断
        return "";
    } else if(arr.length === 1){
        return arr[0];
    }
    let res = "",temp = "";
    for(let i = 0; i < arr[0].length; i++){  
        // 以数组第一个元素为标准
        res += arr[0][i];
        for(let j = 1; j < arr.length; j++){  
            // 如果后面所有元素都以该前缀开头,则前缀++
            if(arr[j].indexOf(res) !== 0){
                return temp  // 否则返回 temp
//Array.indexOf()方法返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回-1。
            }
        }
        temp = res;
    }
    return res;
}
console.log(longestCommonPrefix(["dog","racecar","car"])) //

console.log(longestCommonPrefix(["flower","flow","flight"]))
//fl

24.加油站问题-贪心算法

一辆汽车加满油后可行驶n公里。旅途中有若干个加油站。设计一个有效算法,指出应在哪些加油站停靠加油,使沿途加油次数最少。对于给定的n(n <= 5000)和k(k <= 1000)个加油站位置,编程计算最少加油次数。并证明算法能产生一个最优解。
要求:

输入:第一行有2个正整数n和k,表示汽车加满油后可行驶n公里,且旅途中有k个加油站。接下来的1 行中,有k+1 个整数,表示第k个加油站与第k-1 个加油站之间的距离。第0 个加油站表示出发地,汽车已加满油。第k+1 个加油站表示目的地。

输出:输出编程计算出的最少加油次数。如果无法到达目的地,则输出”NoSolution”。

function greedy(n, k, arr){  // n:加满可以行驶的公里数; k:加油站数量; arr:每个加油站之间的距离数组
	if (n == 0 || k == 0 || arr.length == 0 || arr[0] > n) {
		return "No Solution!";  
        // arr[0] > n :如果第一个加油站距离太远,也无法到达
	};
	let res = 0, distance = 0;  
    // res:加油次数;distance:已行驶距离
	for(let i = 0; i <= k; i++){
		distance += arr[i];
		if (distance > n) {  // 已行驶距离 > 加满可以行驶的公里数
			if(arr[i] > n){  // 如果目前加油站和前一个加油站的距离 > 加满可以行驶的公里数,则无法到达
				return "No Solution!";
			};
			// 可以在上一个加油站加油,行驶到目前的加油站i:
			distance = arr[i];
			res++;  // 加油次数+1
		}
	}
	return res;
}
let arr = [1,2,3,4,5,1,6,6];
console.log(greedy(7,7,arr))  // 4

25.用正则实现trim() 清除字符串两端空格

String.prototype.trim1 = function(){
	// return this.replace(/\s*/g,"");  // 清除所有空格
	return this.replace(/(^\s*)|(\s*$)/g,"");  // 清除字符串前后的空格
};
console.log("  hello word ".trim1())  // "hello word"

26.岛问题:判断有几个岛

一个矩阵中只有0和1两种值,每个位置都可以和自己的上、下、左、右 四个位置相连,如果有一片1连在一起,这个部分叫做一个岛,求一个矩阵中有多少个岛?

/*
举例:  下面这个矩阵中有4个岛。
let arrIsland = [
	[0,0,1,0,1,0],
	[1,1,1,0,1,0],
	[1,0,0,1,0,0],
	[0,0,0,0,0,1]
];
实现思路:
1.遍历整个矩阵,当 arr[i][j] === 1 时,将其值改成2,同时 岛的数量 +1;
2.将这个位置的 上、下、左、右 的四个位置的值都检查一遍,(递归实现)
位置 i,j超出边界 或 该位置的值 不等于1,返回;
不是上面的情况,则:将该位置的值改为 2 ,再重复步骤2。
*/
function islandCount(arr){
	if (!arr || arr.length === 0) {
		return;
	};
	let N = arr.length, M = arr[0].length, res = 0;
	for(let i = 0; i < N; i++){
		for(let j = 0; j < M; j++){
			if (arr[i][j] === 1) {
				++res;
				infect(arr,i,j,N,M);
			}
		}
	}
	return res;
}
function infect(arr,i,j,N,M){
	if (i < 0 || j < 0 || i >= N || j >= M || arr[i][j] !== 1) {
		return;
	};
	arr[i][j] = 2;
	infect(arr,i,j-1,N,M);
	infect(arr,i+1,j,N,M);
	infect(arr,i,j+1,N,M);
	infect(arr,i-1,j,N,M);
}
let arrIsLand = [
	[0,0,1,0,1,0],
	[1,1,1,0,1,0],
	[1,0,0,1,0,0],
	[0,0,0,0,0,1]
];
console.log(islandCount(arrIsLand));  // 4

27.将数字12345678转化成RMB形式:12,345,678

思路:将字符串切割成数组再反转,遍历数组,加入辅助数组,当数组长度为3的倍数,再向辅助数组加入 “,”。

function RMB(str){
	let arr = str.split("").reverse();
	let res = [];
	for(let i = 0; i < arr.length; i++){
		res.push(arr[i]);
		if ((i + 1) % 3 === 0) {
			res.push(",");
		}
	}
	return res.reverse().join("");
}
console.log(RMB("12345678"))//12,345,678

28.删除相邻相同的字符串

function delSrt(str){
	let res = [], nowStr;
	for(let i = 0; i < str.length; i ++){
		if (str.charAt(i) != nowStr) {
			res.push(str.charAt(i));
			nowStr = str.charAt(i);
		}
	}
	return res.join("");
}
console.log(delSrt("aabcc11"))//abc1

29.宣讲会安排

一些项目要占用一个会议室宣讲,会议室不能同时容纳两个项目的宣讲。 给你每一个项目开始的时间和结束的时间(数组,里面是一个个具体的项目),你来安排宣讲的日程,要求会议室进行的宣讲的场次最多。返回这个最多的宣讲场次。

步骤:

先按照会议的end时间升序排序;
排除了因为正在进行会议而无法进行的会议(now > obj[i].start);
会议能举行,则 res++,并且更新目前时间now (now = obj[i].end;)。

function getMostCount(obj){
	if (!obj || obj.length < 1) {
		return;
	};
	obj.sort(sortEndTime);
	let res = 1, now = obj[0].end;
	for(let i = 1; i < obj.length; i++){
		if (now < obj[i].start) {
			res++;
			now = obj[i].end;
		}
	}
	return res;
}
// 自定义排序法
function sortEndTime(obj1,obj2){
	return obj1.end - obj2.end;
}
//这里返回的是他们的差值,如果是大于0的值,就会将2排在前面,如果小于0,就会将1排在前面,如果是0的话,就随便。(冒泡排序法!!)
var obj = [
	{start:6,end:8},
	{start:7,end:9},
	{start:11,end:12},
	{start:10,end:14},
	{start:16,end:18},
	{start:17.5,end:21},
	{start:15,end:17},
	{start:22,end:23}
];
console.log("最大场次:" + getMostCount(obj));//最大场次:5

30.汉诺塔问题

汉诺塔(Tower of Hanoi),又称河内塔,是一个源于印度古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。(源自百度百科)

把A杆上的金盘全部移到C杆上,并仍保持原有顺序叠好。操作规则:每次只能移动一个盘子,并且在移动过程中三根杆上都始终保持大盘在下,小盘在上,操作过程中盘子可以置于A、B、C任一杆上。

思路:

递归解决:把问题转化为规模缩小了的同类问题的子问题;
明确递归结束的条件(base case):n == 1
其他过程:from:来源地;to:目的地;help:辅助。

function hanoiProcess(n,from,to,help){
	if (n < 1) {
		return;
	}
	if (n == 1) {  // 最后一个从from移到to
		console.log("Move 1 from " + from + " to " + to);
	} else{
		hanoiProcess(n-1, from, help, to);  // 前n-1个从from移到help上,可以借助to
		console.log("Move "+ n +" from " + from + " to " + to);
		hanoiProcess(n-1, help, to, from);  // 再把n-1个从help移到to,可以借助from
	}
}
hanoiProcess(3, "左", "右", "中");

结果:

Move 1 from 左 to 右
Move 2 from 左 to 中
Move 1 from 右 to 中
Move 3 from 左 to 右
Move 1 from 中 to 左
Move 2 from 中 to 右
Move 1 from 左 to 右

31.母牛生母牛问题

母牛每年生一只母牛,新出生的母牛成长三年后也能每年生一只母牛,假设不会死。求N年后,母牛的数量。

思路:

因为新生的母牛,只有等到第四年才能生小母牛。所以前4年,只有原来的一头母牛每年生一头。
第五年以后,除了有前一年的牛数量,还有三年前的牛可以生新的小牛。(最近3年内生的牛还不能生)

function cow(n){
	if (n < 1) {
		return;
	};
	let count = 0;
	if (n > 4) {
		count = cow(n-1) + cow(n-3);
	} else{
		count = n;
	}
	return count;
}
let n = 7;
console.log(n + " 年后,牛的数量是: " + cow(n))
// 7 年后,牛的数量是: 13

32.切割金条-贪心算法

一块金条切成两半,是需要花费和长度数值一样的铜板的。比如长度为20的金条,不管切成长度多大的两半,都要花费20个铜板。一群人想整分整块金条,怎么分最省铜板?

例如,给定数组{10,20,30},代表一共三个人,整块金条长度为 10+20+30=60。金条要分成10,20,30三个部分。 如果先把长度60的金条分成10和50,花费60。再把长度50的金条分成20和30, 花费50。一共花费110铜板。但是如果先把长度60的金条分成30和30,花费60。再把长度30 金条分成10和20,花费30。一共花费90铜板。 输入一个数组,返回分割的最小代价。

思路:

这个题是哈夫曼编码问题,想把金条切成规定的多少段,选择一个怎样的顺序能让代价最低。我们可以认为每一块金条长度是一个叶节点,怎么决定叶节点的合并顺序才能让整体的合并代价最低。而两个叶节点合并之后产生的和就是它的合并代价。

也就是说,这个题是求所有非叶节点的值加起来最低。这个题整体就转化为:给了叶节点,选择一个什么合并顺序,能够导致非叶节点整体的求和最小。所以解题时可以反过来,把“一整条金条该如何切割”,换为“已知需要切割的长度,如何使之加起来的代价最小”。

/* 	步骤:
先把需要分割的长度值,加入小根堆;
取出小根堆里两个最小的值,合并后的值再加入小根堆;
一直重复第二步,直到堆里没有值,可得到最小的和。
*/
function lessMoney(arr){
	if (!arr || arr.length < 1) {
		return;
	};
	let res = 0;
	while(arr.length > 1){
		let cur = pollHeap(arr) + pollHeap(arr);
		res += cur;
		addHeap(arr,cur,arr.length-1);
	}
	return res;
}
function minHeap(arr){  // 建立小根堆
	for(let i = 0; i < arr.length; i++){
		while(arr[i] < arr[parseInt((i - 1)/2)]){
			swap(arr,i,parseInt((i - 1)/2));  // 交换位置
			i = parseInt((i - 1)/2);
		}
	}
	
};
function pollHeap(arr){  // 取出一个值
	minHeap(arr);
	return arr.shift();
};
function addHeap(arr,cur){  // 加入一个值
	arr.push(cur);
	let i = arr.length-1;
	while(arr[i] < arr[parseInt((i - 1)/2)]){
		swap(arr,i,parseInt((i - 1)/2))
		i = parseInt((i - 1)/2);
	}
}
function swap(arr,i,j){  // 交换位置
	let temp = arr[i];
	arr[i] = arr[j];
	arr[j] = temp;
}
console.log(lessMoney([20,30,10,50]));  // 200
// 10+20=30 30+30=60 60+50=110;  30+60+110=200

33.股票最大利润

假设有一个数组,它的第i个元素是一支给定的股票在第i天的价格。
如果你最多只允许完成一次交易,设计一个算法来找出最大利润。
实例:
Input: [7,1,5,3,6,4]
Output: 7
Explanation: Buy on day 2 (price = 1) and sell on day 3 (price = 5), profit = 5-1 = 4.
Then buy on day 4 (price = 3) and sell on day 5 (price = 6), profit = 6-3 = 3.
Input: [7,6,4,3,1]
Output: 0
Explanation: In this case, no transaction is done, i.e. max profit = 0.

function maxProfit(prices) {
    var sum=0;
    let len=prices.length;
    for (let i = 0; i <= len; i++ ) {
        if(prices[i]<prices[i+1]){
            sum+=prices[i+1]-prices[i];
        }
    }
    return sum;
};
var arrr=[7,6,5,4,3,2,1]
console.log(maxProfit(arrr))

34.将十进制表示的rgba字符串转为十六进制表示的字符串.

透明度直接用字符串表示即可。

    输入:rgba(125,125,125,0.4)
    输出:["#ffffff",0.4]

<script>
// 将十进制的rgba表示转为16进制
let str = "rgba(125,25,255,0.4)";
let rgba = str.match(/^rgba\((\d*),(\d*),(\d*),(.*)\)$/);
// console.log(rgba);
let result = '#'
for (let i = 1; i < rgba.length - 1; i++) {
    // console.log(parseInt(rgba[i]).toString(16));
    result += parseInt(rgba[i]).toString(16);
}
console.log([result, rgba.slice(-1).toString()]);
</script>

35.将URL中的query string转为json格式

//url为:http:www.jsdk.demo.html?x=2&y=3&y=4&y=5&z=0
//输出:{x:2, y:[3,4,5], z:0}
<script>
        // 将URL中的query string转为json格式   例如[x:1,y:[3,4,5],z:0]
        // let url = "http:www.jsdk.demo.html?x=2&y=3&y=4&y=5&z=0";
        let query = window.location.search.substring(1).split('&');
        let json = {};
        for (let i = 0; i < query.length; i++) {
            let temp = query[i].split('=');
            if (json.hasOwnProperty(temp[0])) {
                console.log(typeof (json[temp[0]]));
                if (typeof (json[temp[0]]) === 'number') {
                    json[temp[0]] = [json[temp[0]], parseInt(temp[1])];
                    console.log(json[temp[0]]);
                }
                else {
                    console.log(json[temp[0]]);
                    json[temp[0]].push(parseInt(temp[1]));
                }
            }
            else {
                json[temp[0]] = parseInt(temp[1]);
            }
        }
        console.log(json);
</script>

36.淘汰分数

某比赛已经进入了淘汰赛阶段,已知共有n名选手参与了此阶段比赛,他们的得分分别是a_1,a_2….a_n,小美作为比赛的裁判希望设定一个分数线m,使得所有分数大于m的选手晋级,其他人淘汰。

但是为了保护粉丝脆弱的心脏,小美希望晋级和淘汰的人数均在[x,y]之间。

显然这个m有可能是不存在的,也有可能存在多个m,如果不存在,请你输出-1,如果存在多个,请你输出符合条件的最低的分数线。

输入描述:

输入第一行仅包含三个正整数n,x,y,分别表示参赛的人数和晋级淘汰人数区间。(1<=n<=50000,1<=x,y<=n)输入第二行包含n个整数,中间用空格隔开,表示从1号选手到n号选手的成绩。(1<=|a_i|<=1000)

输出描述:

输出仅包含一个整数,如果不存在这样的m,则输出-1,否则输出符合条件的最小的值。

输入例子1:

6 2 3
1 2 3 4 5 6

输出例子1:

3


37.正则序列

时间限制:C/C++ 1秒,其他语言2秒

空间限制:C/C++ 256M,其他语言512M

我们称一个长度为n的序列为正则序列,当且仅当该序列是一个由1~n组成的排列,即该序列由n个正整数组成,取值在[1,n]范围,且不存在重复的数,同时正则序列不要求排序

有一天小团得到了一个长度为n的任意序列s,他需要在有限次操作内,将这个序列变成一个正则序列,每次操作他可以任选序列中的一个数字,并将该数字加一或者减一。
请问他最少用多少次操作可以把这个序列变成正则序列?

输入描述:

输入第一行仅包含一个正整数n,表示任意序列的长度。(1<=n<=20000)

输入第二行包含n个整数,表示给出的序列,每个数的绝对值都小于10000。

输出描述:

输出仅包含一个整数,表示最少的操作数量。

输入例子1:

5
-1 2 3 10 100

输出例子1:

103

38.公司食堂

时间限制:C/C++ 2秒,其他语言4秒

空间限制:C/C++ 256M,其他语言512M

小美和小团所在公司的食堂有N张餐桌,从左到右摆成一排,每张餐桌有2张餐椅供至多2人用餐,公司职员排队进入食堂用餐。小美发现职员用餐的一个规律并告诉小团:当男职员进入食堂时,他会优先选择已经坐有1人的餐桌用餐,只有当每张餐桌要么空着要么坐满2人时,他才会考虑空着的餐桌;

当女职员进入食堂时,她会优先选择未坐人的餐桌用餐,只有当每张餐桌都坐有至少1人时,她才会考虑已经坐有1人的餐桌;
无论男女,当有多张餐桌供职员选择时,他会选择最靠左的餐桌用餐。现在食堂内已有若干人在用餐,另外M个人正排队进入食堂,小团会根据小美告诉他的规律预测排队的每个人分别会坐哪张餐桌。

输入描述:

第一行输入一个整数T(1<=T<=10),表示数据组数。

每组数据占四行,第一行输入一个整数N(1<=N<=500000);

第二行输入一个长度为N且仅包含数字0、1、2的字符串,第i个数字表示左起第i张餐桌已坐有的用餐人数;

第三行输入一个整数M(1<=M<=2N且保证排队的每个人进入食堂时都有可供选择的餐桌);

第四行输入一个长度为M且仅包含字母M、F的字符串,若第i个字母为M,则排在第i的人为男性,否则其为女性。

输出描述:

每组数据输出占M行,第i行输出一个整数j(1<=j<=N),表示排在第i的人将选择左起第j张餐桌用餐。

输入例子1:

1
5
01102
6
MFMMFF

输出例子1:

2
1
1
3
4
4

39.最优二叉树II

时间限制:C/C++ 1秒,其他语言2秒

空间限制:C/C++ 256M,其他语言512M

小团有一个由N个节点组成的二叉树,每个节点有一个权值。定义二叉树每条边的开销为其两端节点权值的乘积,二叉树的总开销即每条边的开销之和。小团按照二叉树的中序遍历依次记录下每个节点的权值,即他记录下了N个数,第i个数表示位于中序遍历第i个位置的节点的权值。之后由于某种原因,小团遗忘了二叉树的具体结构。在所有可能的二叉树中,总开销最小的二叉树被称为最优二叉树。现在,小团请小美求出最优二叉树的总开销。

输入描述:

第一行输入一个整数N(1<=N<=300),表示二叉树的节点数。

第二行输入N个由空格隔开的整数,表示按中序遍历记录下的各个节点的权值,所有权值均为不超过1000的正整数。

输出描述:

输出一个整数,表示最优二叉树的总开销。

输入例子1:

5
7 6 5 1 3

输出例子1:

45

例子说明1:

最优二叉树如图所示,总开销为71+65+51+13=45。

40.猜数字大小

猜数字游戏的规则如下:

每轮游戏,我都会从 1 到 n 随机选择一个数字。 请你猜选出的是哪个数字。
如果你猜错了,我会告诉你,你猜测的数字比我选出的数字是大了还是小了。

你可以通过调用一个预先定义好的接口 int guess(int num) 来获取猜测结果,返回值一共有 3 种可能的情况(-1,1 或 0):

-1:我选出的数字比你猜的数字小 pick < num
1:我选出的数字比你猜的数字大 pick > num
0:我选出的数字和你猜的数字一样。恭喜!你猜对了!pick == num

返回我选出的数字。

//var guess = function(num) {}
var guessNumber = function(n) {
    let left = 1, right = n;
    while (left < right) { // 循环直至区间左右端点相同
        const mid = Math.floor(left + (right - left) / 2); 
        if (guess(mid) <= 0) {
            right = mid; // 答案在区间 [left, mid] 中
        } else {
            left = mid + 1; // 答案在区间 [mid+1, right] 中
        }
    }
    // 此时有 left == right,区间缩为一个点,即为答案
    return left;
};

41.爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

var climbStairs = function(n) {
        let p = 0, q = 0, r = 1;
    for (let i = 1; i <= n; ++i) {
        p = q;
        q = r;
        r = p + q;
    }
    return r;
};

42.爬楼梯的最少成本

数组的每个下标作为一个阶梯,第 i 个阶梯对应着一个非负数的体力花费值 cost[i](下标从 0 开始)。

每当爬上一个阶梯都要花费对应的体力值,一旦支付了相应的体力值,就可以选择向上爬一个阶梯或者爬两个阶梯。

请找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯。

var minCostClimbingStairs = function(cost) {
    const n = cost.length;
    const dp = new Array(n + 1);
    dp[0] = dp[1] = 0;
    for (let i = 2; i <= n; i++) {
        dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
    }
    return dp[n];
};
var minCostClimbingStairs = function(cost) {
    const n = cost.length;
    let prev = 0, curr = 0;
    for (let i = 2; i <= n; i++) {
        let next = Math.min(curr + cost[i - 1], prev + cost[i - 2]);
//当前花费的最小值就是前一个的花费加之前的花费和前两个台阶的花费+之前的花费的最小值 
        prev = curr;
        curr = next;
    }
    return curr;
};

43.贩卖战利品

在游戏中,击败魔物后,薯队长获得了N件宝物,接下来得把这些宝物卖给宝物回收员来赚点小钱。这个回收员有个坏毛病,每次卖给他一件宝 物后,之后他就看不上比这件宝物差的宝物了。在这个世界中,衡量宝物的好坏有两个维度,稀有度X和实用度H,回收员在回收一个宝物A 后,下一个宝物的稀有度和实用度都不能低于宝物A。那么薯队长如何制定售卖顺序,才能卖给回收员宝物总个数最多。

var num = readline();
var arr= [];
var n = null;
while(n = readline()){
    n=n.split(" ").map(item => {
        return Number(item)
    })
    arr.push(n)
}
arr.sort((d1, d2) => {
        return d1[0] != d2[0] ? d1[0] - d2[0] : d1[1] - d2[1]
      });
function LIS(num, arr) {
      var temp = [];
      for (var i = 0; i < num; i++) {
        temp.push(arr[i][1]);
      }
      let newArr = new Array(num);
      newArr[0] = temp[0]
      let end = 0;
      for (var k = 0; k < num; k++) {
        if (temp[k] > newArr[end]) {
          end++;
          newArr[end] = temp[k];
        } else {
          let left = 0 ;
          let right = end ;
          while(left < right){
            let mid = left + ((right - left) >> 1);
            if(newArr[mid] < temp[k]){
              left = mid + 1;
            } else {
              right = mid;
            }
          }
          newArr[left] = temp[k]
        }
      }
      return end + 1
    }
      console.log(LIS(num, arr)) ;

44.笔记草稿

薯队长写了一篇笔记草稿,请你帮忙输出最后内容。
输入字符包括,“(” , “)” 和 "<"和其他字符。
其他字符表示笔记内容。
()之间表示注释内容,任何字符都无效。 括号保证成对出现。
"<“表示退格, 删去前面一个笔记内容字符。括号不受”<"影响 。

let line = readline();
const filterText = (line) => {
    let a = []
    for (let item of line) {
        if(item === ')'){
            while(a.pop()!=='(');
        }else if(item === '<'){
            a.pop()
        } else {
            a.push(item)
        }
    }
    return a.join('');
}
console.log(filterText(line));

45.笔记精选

薯队长写了n篇笔记,编号从1~n,每篇笔记都获得了不少点赞数。

薯队长想从中选出一些笔记,作一个精选集合。挑选的时候有两个规则:

  • 不能出现连续编号的笔记。
  • 总点赞总数最多

如果满足1,2条件有多种方案,挑选笔记总数最少的那种

var a = readline();
var c = readline();
c = c.split(" ");
    var b = c.map(item =>{
      return Number(item)
    })
var point = [];
var num = [];
point[0] = b[0];
num[0] = 1;
point[1] = Math.max(b[0],b[1]);
num[1] = 1;
for (var i = 2; i < b.length; i++) {
   if(b[i] + point[i - 2] > point[i - 1]){
       point[i] = b[i] + point[i - 2];
       num[i] = 1 + num[i-2];
    } else {
       point[i] = point[i - 1];
       num[i] = Math.min(num[i - 1] , 1 + num[i - 2])
    }
}
var res = [];
res.push(Math.max(...point));
let record = point.indexOf(res[0]);
res.push(num[record])
console.log(res[0] , res[1]) 
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

捣蛋龙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值