面试--- 常用算法总结

 

 

算法一般都是,数组排序, 数据查找, 数据计算 ,链表 , 拷贝。。。

开发来说,算法起码要会简单的常规的,真正的算法工程师才要懂那些难的。。。

 

力扣平台上很多算法总结

https://leetcode-cn.com/

 

常用的排序算法

https://developer.aliyun.com/article/772212?spm=5176.11533457.J_1089570.48.17ed5333JGS7fc

 

 

 

排序

一  冒泡

  1  原理

     通过多次嵌套循环比对交换位置;最终将有序的小的数字排在前面,大的排在后面;每一趟排序完成后会确定一个数字的最终位置。

     冒泡这种算法时间复杂度高,当需要排序的元素较多时,程序运行时间很长。

 

   2  demo

js 实现   

let arr=[6,3,8,2,9,1];
 let temp=0;
 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]){
          temp=arr[j];
          arr[j]=arr[j+1];
          arr[j+1]=temp;
        }
      }
    }

   console.log(arr);

java 实现
protected void sort(int[] nums) { if (nums == null || nums.length < 2) { return; } for (int i = 0; i < nums.length - 1; i++) {//嵌套循环外部循环一次,内部循环全部 for (int j = 0; j < nums.length - i - 1; j++) {//以内部循环为准比对,但是要每次减掉外部的循环的次数缩小范围 if (nums[j] > nums[j + 1]) { int temp = nums[j]; nums[j] = nums[j + 1]; nums[j + 1] = temp; } } } }

 

   3   demo 

冒泡let maopao=(nums)=>{
//let aNew=[];
for(let i=0;i<a.length-1;i++){//外面执行一次
let temp;
for(let j=i+1;j<a.length;j++){//里面全部执行,比对用temp替换位置,并按新顺序存到新的集合中 .思想就是拿到一个值和后面的所有值比对大小临时替换
if(nums[i]>nums[j]){

temp=nums[i];
nums[i]=nums[j];
nums[j]=temp;


}
}
//aNew.push(temp);
}
console.log(nums);
}

动画演示

 

 

 

 

 

二   快排--常问

 1  原理

    就是一个无序的数组,拿一个数(第一个或最后一个)当作参考;

   用双向游标i/j进行排序(分别从左向右找比基数大的一个数;从右往做找比基数小的一个数;把2个数字交换位置,重复执行知道游标相等)后,

   把所有比他小的放到左边,比他大的放到右边; 然后分别对左右两边数组通过递归的方法,重复上述动作排序。

    PS:基准总是取序列开头的元素. 快排是对冒泡排序算法的一种改进

    通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,

  然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

 

 

核心思路 :

1. 在数组中选一个基准数(通常为数组第一个);

2  用双向游标i/j进行排序(分别从左向右找比基数大的一个数;从右往做找比基数小的一个数;把2个数字交换位置,重复执行知道游标相等)
3. 将数组中小于基准数的数据移到基准数左边,大于基准数的移到右边;
4. 对于基准数左、右两边的数组,不断重复以上两个过程,直到每个子集只有一个元素,即为全部有序。

 

 

快排 :就是在无序数组中循找 基数正确位置的过程。

1 选一个基数一般都是第一个,临时存起来。

2 2个游标循环迭代 ,j先从右到左 把比基数小的都放到基数左边; i在从左到右把比基数大的都放到基数右边。

3 直到 i==j 结束,此时i的位置就是 基数应该放的位置 ,基数赋值给 [i]=temp 。

4 左右两部分分别 迭代重复上述的动作 直到每个子集只有一个元素,即为全部有序。

 

 

 

 

 

 2   demo

console.time("快排");  //可以记录开始执行时间

     

function quicksort(a,left,right){
if(left>right){ //一定要有这个判断,因为有递归left和i-1,若没有这个判断条件,该函数会进入无限死错位递归
return;
}

var i=left,
j=right,
jizhun=a[left]; //基准总是取序列开头的元素

while(i!=j){ //该while的功能为每一趟进行的多次比较和交换最终找到位置k。当i==j时意味着找到k位置了
while(a[j]>=jizhun&&i<j){j--} //只要大于等于基准数,j指针向左移动直到小于基准数才不进入该while。i<j的限制条件也是很重要,不然一直在i!=j这个循环里,j也会越界
while(a[i]<=jizhun&&i<j){i++} //只要小于等于基准数,i指针向右移动直到大于基准数才不进入该while。等于条件也是必要的,举例[4,7,6,4,3]验证一下是两个4交换
if(i<j){ //如果i==j跳出外层while
t=a[i];
a[i]=a[j];
a[j]=t
}
}

a[left]=a[i];//交换基准数和k位置上的数
a[i]=jizhun;

quicksort(a,left,i-1);
quicksort(a,i+1,right);
}

var array=[4,7,2,8,3,9,12];
console.log(quicksort(array,0,array.length-1));//排完序后再看array是[2, 3, 4, 7, 8, 9, 12]

 

console.timeEnd("快排");  //可以记录结束执行时间

 

 

 

 

 

或者自己理解的写法>>>

 

 

let testSor=(nums,i,j)=>{//数组和 从右到左的结束位置 j-- ,从左到右开始位置 i++;目的寻找基数的正确位置
let first=nums[i]; //一般数组第一个作为基数比对
while(i<j){ //当i===j的时候找到了本次基数的正确位置,把基数赋值给 i的当前位置值 并且返回基数的位置(因为左右两边的数据还要继续按此规则迭代,开始和结束位置参数依赖于本次基数位置i)
if(i<j&&nums[j]>=first){ //从右往左比对基数,当比基数大继续循环
j--;
}
nums[i]=nums[j]; //当比基数小就替换到左边
if(i<j&&nums[i]<=first){ //从左往右比对基数,当比基数小继续循环
i++;
}
nums[j]=nums[i]; //比基数大就替换到右边
}
nums[i]=first; //i===j的时候跳出循环
return i; //当前位置就是 基数的位置
}
let digui=(nums,i,j)=>{//递归
if(i<j){
let curIndex=testSor(nums,i,j); //每次循环拿到基数最佳位置,供下次循环使用
digui(nums,i,curIndex-1); //左边都是比基数小的,所以位置到基数位置curIndex-1 截止
digui(nums,curIndex+1,j); //右边都是比基数大的,所以位置从基数curIndex+1 开始
}
}

 

// 测试
let nums=[5,6,6,2,9,3];
digui(nums,0,nums.length-1);
console.log(nums);

 

 

 

 

 

 

 

三   插入排序

 

无序数组最终变成有序数组,不是说插入一个数字。

 

思路:

  1. 从第一个元素开始,该元素可以认为已经被排序;
  2. 取出下一个元素比较,如果第一个该元素(已排序)大于新元素,将第一个元素和新元素替换位置;
  3. 重复(循环)步骤2,直到找到已排序的元素小于或者等于新元素的位置
  4. 将新元素插入到该位置后;
  5.  重复步骤2~5。 也就是新元素后面的元素和它继续比对。    

 

 

 动画演示

 

 

 

 

 demo

protected void sort(int[] nums) {
if (nums == null || nums.length < 2) {
return;
}
for (int i = 0; i < nums.length - 1; i++) { // 核心思路是拿当前值(从第一个开始) 和 下一个值进行比较。整个过程循环。

//当前值下标(因为下标会变化所以不用直接赋值)
int preIndex = i;

//下一个值
int curr = nums[i + 1];

//当下个值比当前值小 进行数字交换
while (preIndex >= 0 && curr < nums[preIndex]) {
// 第一步: 把当前值替换给下一个值
nums[preIndex + 1] = nums[preIndex];

//第二步,移动下标,为了把后面的值付给当前值
preIndex--;
}
// 后面的值替换给当前值, 整个就进行了替换
nums[preIndex + 1] = curr;
}
}

 

 

 

 

 

 

 

 

 

 

 

 

 

查找

一  二分法查找

1  原理
需要是有序的数组, 那要查找的值,和序列最中间的值对比,(加入是升序),那么小于对比的值 就往前面查找,
如果大于这个值,就往后面查找。

取中间值为比较对象,若等于关键字,则查找成功;若大于关键字,则在比较对象的左半区继续查找;若小于关键字,则在比较对象的右半区继续查找。不断重复上述过程直到查找成功,若所有查找区域无记录则查找失败。

 

 

 

2  demo

private int halfSearch(int[] arr, int target){
        int min = 0;//数组最小索引
        int max = arr.length - 1;//数组最大索引
        int mid = (min + max) / 2;//数组中间索引
        
        int i = 0;
        while(true){
            System.out.println("第" + ++i + "次,min:" + min + ";mid:" + mid + ";max:" + max);
            //跟中间值进行比较
            if (target > arr[mid]) {
                min = mid + 1;
            }
            else if (target < arr[mid]) {
                max = mid - 1;
            }
            else if(target == arr[mid]){
                return mid;
            }
            //重新取中间索引
            mid = (min + max) / 2;
            //如果最小索引大于最大索引说明有问题,直接返回
            if (min > max) {
                return -1;
            }
        }
    }

    

         测试:::

        int[] arr = {10, 12, 15, 17, 19, 20, 22, 23, 24, 25, 30, 31, 32, 33, 34, 35, 40, 41, 42, 43, 44, 45, 46};
        int target = 32;
        
        int index = halfSearch(arr, target);
        System.out.println(index);

 

 

 

 

或者>>>>

 

let str=(nums,target)=>{
let start=0, end=nums.length-1, mid=Math.floor(start+end)/2;
while(start<=end){

if(target===nums[mid]){ return mid;}

if(target>nums[mid]){
start=mid+1;
}else{
end=mid-1;
}

}
}

 

 

或者>>>>

 

let testNums= (arr,num)=>{
let
st = 0,
end = arr.length-1;


while(st<=end){

let mid = Math.floor((st+end)/2); //因为每次循环st和end都在变化 ,mid位置也在变化 所以要放到循环里面

if(num==arr[mid]){
return mid;
}else if(num>arr[mid]){
st = mid+1;
}else{
end = mid-1;
}
}

}

 

 

 

 

 

 

 

 

 

 

 

 

 

计算

 

一   计算2数之和,从数组中找出2个数字加起来等于目标值

 

 

let testReulst=(nums,target)=>{  //nums数组[],target和值
let maps=new Map();   //es6的动态hash表,set/get/has
for(let i=0;i<nums.length;i++) {
let mus=target-nums[i];   //关键1---因为相加等于targe
if(mus>0){  //比target小可能加起来
if(!maps.has(mus))   //如果没有说明当前被减掉的nums[i]在加上另外一个数字可以等于target  
{
maps.set(nums[i],i);   //关键2---先确定其中一个值,比如9-2=7
}
else{  
return [maps.get(mus),i]   //关键3---找到另外一个数字(当前的nums[i])和之前存的数字的下标,比如9-7=2;
}
}
}
}

 

 
 
二     数组/集合反转 算法
ps: 集合内容反转直接用自带的 .reverse() 或者 先循环找到下标index后在一个个的加入到新的数组;
     集合转成字符串直接.toString ; 字符串转成集合直接for循环字符串然后一个个的加入到数组中;
 
let testNums=(a)=>{
let inde=[];
for(let i=0;i<a.length;i++){ // 先循环数组的index 倒叙加入到一个新集合
inde.unshift(i);
}

let news=[];
for(let j=0;j<inde.length;j++){  // 在循环index的集合,里面把对应数组的值找出来加入到新的集合中就是反转

news.push(a[inde[j]]);
}
}

 

 

 

 

 

 

 

 

 

 

三   判断是否是回文数

 

let testNum=(nums)=>{


let numStr=[...String(nums)] // 回文数,从左到右,从右到左 数字都是一样的;(负数不一样) ; 先数字转字符串到数组,然后从前到后从后到前循环比对,只要有一个不同就是错的


for(let i=0,j=numStr.length-1;i<numStr.length,j>=0;j--,i++){


if(numStr[i]!==numStr[j]){


return false
}
}
return true
}

 

 

 

或者

 

let isPalind = function(x) {
let str=`${Math.abs(x)}`; //先吧数字转成字符串 ,方式 1 后面+'' 2 String(x) 3 `${Math.abs(x)}`
let strNums=str.split(''); //字符串转成数组 ,方式 1 split('') 2 for循环字符串然后把值加到一个新集合 3 [...String(x)] 解构,数字的话需要先String
let newX=strNums.reverse(); // 回文数从左到右,从右到左 都是一样的,那么反转后也是一样的! 数组反转
return Number(newX.join(''))==x //兼容负数,转成数字后和原数据比对
};

 

 

 

 

 

 

 

 

 

 

 

 

链表


 

三   反转链表

 

let testList=(lists)=>{ //链表不像数组没有索引,只能通过next来遍历,改变next之前要存起来

 

let temp,pre=null,cur=lists; //当next是null的时候完毕,pre赋给next


while(cur.next!==null){

temp=cur.next;
cur.next=pre;
pre=cur;
cur=temp;
}

return pre;
}

 

或者 解构的方式

var reverseList = function(head) {
[curr, pre] = [head, null];
while(curr) [curr.next, pre, curr] = [pre, curr, curr.next];
return pre;
};

 

 

 

 

 

四    实现链表的核心思路

 

 

实现连表核心思路: 实现一个ArrayList 里面有一个方法比如find, findLast 等 比如 var myList = new ArrayList()
var arr = ['A', 'B', 'C', 'D', 'E', 'F', 'G']

arr.forEach(item => myList.append(item)) , 链表中的节点类型描述如下:

class Node {
constructor(data) {
this.data = data; // 节点的数据域
this.prev = null; // 节点的指针域
this.next = null; // 节点的指针域
}
}

用prev和next两个指针域是为了实现双向链表,

在实现单链表时,prev指针并没有用到。

单链表是使用带有表头节点的形式来实现的;

实现其中方法的核心思想: 当前节点的next指针不为空就一直向下遍历。

 

例如 findLast() {
let currNode = this.head;

while (currNode.next) {
currNode = currNode.next;
}

return currNode;
}

 

// 在尾部添加元素 ,也是给next节点
append(element) {


let newNode = new Node(element);
let currNode = this.findLast();

currNode.next = newNode;


this.size++;
}

 

 

 

 

 

 

 

 

 

 

 

拷贝 

 

 

一   浅拷贝:

只拷贝了原对象的地址,新对象和原对象指向同一个引用,改变其中一个另外一个会发生变化。

常用的方式 1 对象复制 2 Object.assign() 3 ...obj 扩展运算符

 

 

 方式 1 对象/数组赋值 let a=[1,2,3] ; let b=a; b[0]=9; console.log(b); --[9,2,3] ; console.log(a); ---[9,2,3] 2 Object.assign 3 ...扩展运算符

 

 

 

 

二      深拷贝

深拷贝-----深拷贝主要是将另一个对象的属性值拷贝过来之后,另一个对象的属性值并不受到影响,因为此时它自己在堆中开辟了自己的内存区域,不受外界干扰。
浅拷贝主要拷贝的是对象的引用值,当改变对象的值,另一个对象的值也会发生变化。

 

 

  方式 一 ...扩展运算符:  let a=[1,2,3] ; let b=[...a]; b[0]=9; console.log(b); --[9,2,3] ; console.log(a); ---[1,2,3] b是分配了独立的内存地址,修改不会影响到a

 

  方式二 Object.assign:  let o={'a':1,'b':2,'c':3}; let b=Object.assign({},o); b.a=9; console.log(b.a); console.log(o.a);

 

  方式三 slice截取: let a=[1,2,3] ; let b=a.slice(); b[0]=9; console.log(b[0]); console.log(a[0]);

 

  方式四 数组concat: let a=[1,2,3]; let b=[] ; let c=b.concat(a); c[0]=6; console.log(a[0]); console.log(c[0]);

 

  方式五 for循环 但是这5种方式都是简单深度拷贝 ,只能拷贝一层节点。 当对象里面 还有对象 {a:1,b:{d:1,e:2},c:9} ; 数组里面还嵌套的数组 [1,[3,4],2,3,4] 只能拷贝一层的节点,里面的无法深拷贝,值变化会影响另外一个。

 

方式六 粗暴深拷贝 粗暴深拷贝(抛弃对象的constructor)
使用jsON.stringify和jsON.parse实现深拷贝:JSON.stringify把对象转成字符串,再用JSON.parse把字符串转成新的对象;
缺陷:它会抛弃对象的constructor,深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成Object;这种方法能正确处理的对象只有 Number, String, Boolean, Array, 扁平对象,也就是说,只有可以转成JSON格式的对象才可以这样用,像function没办法转成JSON;

function deepCopy(obj1){
let _obj = JSON.stringify(obj1);
let obj2 = JSON.parse(_obj);
return obj2;
}
var aa = [1, [1, 2], 3, 4];
var bb = deepCopy(aa);
bb[1][0] = 2;
console.log(aa); // 1,1,2,3,4
console.log(bb); // 2,2,2,3,4

 

 

方式七 复杂深拷贝(相对完美)
递归拷贝实现深拷贝,解决循环引用问题 。

 

/** * 判断是否是基本数据类型 * @param value */

function isPrimitive(value){
return (typeof value === 'string' || typeof value === 'number' || typeof value === 'symbol' || typeof value === 'boolean') }

/** * 判断是否是一个js对象 * @param value */

function isObject(value){ return Object.prototype.toString.call(value) === "[object Object]" }

/** * 深拷贝一个值 * @param value */

function cloneDeep(value){ // 记录被拷贝的值,避免循环引用的出现
let memo = {};
function baseClone(value){
let res; // 如果是基本数据类型,则直接返回
if(isPrimitive(value)){ return value; // 如果是引用数据类型,我们浅拷贝一个新值来代替原来的值
}else if(Array.isArray(value))
{ res = [...value]; }
else if(isObject(value))
{ res = {...value}; } // 检测我们浅拷贝的这个对象的属性值有没有是引用数据类型。如果是,则递归拷贝
Reflect.ownKeys(res).forEach(key=>{
if(typeof res[key] === "object" && res[key]!== null){ //此处我们用memo来记录已经被拷贝过的引用地址。以此来解决循环引用的问题
if(memo[res[key]]){ res[key] = memo[res[key]]; }
else{ memo[res[key]] = res[key]; res[key] = baseClone(res[key])
}
}
})
return res;
}
return baseClone(value)
}

 

测试>>>

var objects = [1,{ 'a': 1 }, { 'b': 2 }];
var deep = cloneDeep(objects);
deep[0] = 2;
deep[1].a = 2;
console.log(objects[0]);//1
console.log(deep[0]);//2
console.log(objects[1].a);//1
console.log(deep[1].a);//2

 

 

 

 

 

 

 

 

 

 

 

二叉树

 

核心是

1 搞清反转二叉树的 对象数据结构 ,都有value值和left节点和right节点

2 思想是左右节点交互

3 递归调用

4 如果是用迭代 ,首先就要把二叉树转成数组 ---[tree] 

5  然后for循环 数组,拿到里面的item 。

    item里面有left/right节点 进行交换。

    同时把子节点left/right 对象 在加入到最新的集合中 。 最新集合再拿去循环遍历交换。

 

 

一    二叉树反转

const demo1={
val:8,
left:{val:6,left:null,right:null},
right:{val:7,left:null,right:null}
}

 

let testNums=function(tree){
let temp=tree.left;
tree.left=tree.right;
tree.right=temp;
return tree;
}

 

 

let fuzha=(trees)=>{ // 复杂递归

if(!trees){return null;}

let temp=fuzha(trees.left);
trees.left=fuzha(trees.right);
trees.right=temp;

return trees;

}  

 

 

 

 

 

二   平行二叉树

平衡二叉树: 就是left比自己val小,right比自己val 大 。 结合反转二叉树 应该可以理解

 ----这是它跟普通二叉树的唯一差别。

 

 

 

 

 

 

 

 


 深度遍历 (从子节点--->根节点)

 1  深度就是访问一个节点,先访问他的子节点,到了子节点,又去访问子节点的子节点,所以会一直跑到最深的地方去.

 2  最深的那个访问完了,就会访问他的兄弟,他的兄弟访问完了,就会回退到访问他们的父节点(也就是次深的节点).

 3  次深的访问完了,就会访问次深的兄弟,完了再向上,一直回到根节点。

 

 

  如果是二叉树,这个里面还有前序,中序,后续,无非就是在访问子节点访问当前节点,或者子节点访问,或者访问子节点访问.

  前端实际业务里,子节点一般都是数组,所以不太碰到中序,前序、后序还是会有的。

  

 

 

 

 

 

 

 广度遍历 (根节点--->子节点)

1 广度:定义一个队列,先把根节点放进去,搞一重while循环,条件是数组里还有节点,每一次从队列里拿出第一个,然后访问他,然后把他的子节点全部放进队列,然后进入下一次while,知道队列里的节点为空,就结束了。

 

 

总结:

1   深度就是先访问子节点,一层一层往里面访问,最后在到根节点;   

 

 

 

 

3  广度就是搞个队列,把子节点存起来,每次访问队列里面的一个(找子节点),然后再把子节点存起来,直到队列为空。

 

 

 

 

 

 

 

 

 

 

 

其他

队列: 先进先出 ,数组push进 ,shift出 ;

堆栈:后进先出 , 数组push进 , pop出。

 

 

 

 动态规划: 就是反转递归 ,比较难。 

 map原理:map的时间复杂度是O1, 数组是On。

 

 

 

最后:

常用的东西,比如ES6,react vue用法,防抖节流之类的,要张口就来;

什么react原理,webpack原理,基础算法,最好装一装。

 

 

 

 

 

 

 

posted @ 2019-03-25 20:46  JavAndroidJSql  阅读(228)  评论(0编辑  收藏  举报