漫画算法-笔记

233 阅读12分钟

刷题

1.求两个数的最大公约数

//1. 最小值递减求余法
//直接用最小值一直递减与 samll和big 求余得0则表示找到
function getDiv(a,b){
    let big = a>b ? a:b
    let small = a<b ? a:b
    for (let i = small; i >1; i--) {
        if(small%i == 0 && big%i == 0) {
            return i
        } 
    }
    return i
}

//2. 辗转相除法
// 欧几里得算法:两个正整数a和b(a>b),他们最大公约数等于a除于b的余数c和b之间的最大公约数
// a%b = c , 则 b%c 的最大公约与 a和b的最大公约等值。
//缺点 求余速度慢,而且如果数值很大运算慢
function getDiv(a,b){  
    let big = a>b ? a:b
    let small = a<b ? a:b
    if(a%b == 0){
        return small
    }
    return getDiv(samll,big%small) 
}

//3.更相减损法
//两个正整数a和b(a>b),则a和b的最大公约 等于 a-b的差c和b的最大公约
//缺点如果两个只相差很大 ,要减很久
function getDiv(a,b){  
    let big = a>b ? a:b
    let small = a<b ? a:b
    if(a%b == 0){
        return small
    }
    return getDiv(samll,big-small) 
}

//最优解
//辗转相除法 + 更相减损法 + 位移做求余  
   /* 解法:
     * 当a和b均为偶数,gcb(a,b) = 2*gcb(a/2, b/2) = 2*gcb(a>>1, b>>1)
     * 当a为偶数,b为奇数,gcb(a,b) = gcb(a/2, b) = gcb(a>>1, b)
     * 当a为奇数,b为偶数,gcb(a,b) = gcb(a, b/2) = gcb(a, b>>1)
     * 当a和b均为奇数,利用更相减损术运算一次,gcb(a,b) = gcb(b, a-b), 此时a-b必然是偶数,又可以继续进行移位运算。
     * @param a
     * @param b
     * @return 
     */
function getGreatestCommonDivsor2(a, b) {
    if (a == b) {
        return a;
    }
    var aIsEven = a % 2 === 0
    var bIsEven = b % 2 === 0
    // a&1 == 0说明整数a是偶数,否则为奇数
    if (aIsEven && bIsEven) {
        // 当a和b均为偶数,gcb(a,b) = 2*gcb(a/2, b/2) = 2*gcb(a>>1, b>>1)
        return getGreatestCommonDivsor2(a >> 1, b >> 1) << 1;
    } else if (aIsEven && !bIsEven) {
        // 当a为偶数,b为奇数,gcb(a,b) = gcb(a/2, b) = gcb(a>>1, b)
        return getGreatestCommonDivsor2(a >> 1, b);
    } else if (!bIsEven && bIsEven) {
        // 当a为奇数,b为偶数,gcb(a,b) = gcb(a, b/2) = gcb(a, b>>1)
        return getGreatestCommonDivsor2(a, b >> 1);
    } else {
        // 当a和b均为奇数,利用更相减损术运算一次,gcb(a,b) = gcb(b, a-b), 此时a-b必然是偶数,又可以继续进行移位运算。
        var big = a > b ? a : b;
        var small = a < b ? a : b;
        return getGreatestCommonDivsor2(big - small, small);
    }
} 

2.如果判断一个数是2个整数次幂

答题

//1.使用暴力循环
function isPower2(num) {
    let temp = 1
    while(temp < num ){
        if(temp % 2 == 0){
           return true
        }
        temp = temp * 2
    }
    return false
}

//利用 2的次方 都是第一位为1,后面都是0
//如:
//4二进制为 100
//8二进制为 1000
//又因为 8-1的二进制位 0111   , 1000 & 0111 == 0 刚好等于0
//所以只要n & n-1 求& 一定为0
function isPower2(num) {
    return  num & (num -1)
}

3.最小栈实现

设计一个支持 push,pop,top 操作,并能在常数时间内检索到最小元素的栈。

  • push(x) -- 将元素 x 推入栈中。
  • pop() -- 删除栈顶的元素。
  • top() -- 获取栈顶元素。
  • getMin() -- 检索栈中的最小元素。

解题思路

使用两个栈来记录

  • stack记录数据的信息 push,pop
  • stackMin记录最小值信息:每次有新收据都判断是否比之前栈底小,是则放入,所以栈顶放的都是越来越小的值

代码实现

/**
 * initialize your data structure here.
 */
var MinStack = function() {
    this.stack = [];
    this.min = [];
};

/** 
 * @param {number} x
 * @return {void}
 */
MinStack.prototype.push = function(x) {
    this.stack.push(x);
    //判断x和min当前栈顶的数哪个小,如果x小则把x推入min的栈顶
    if(this.stackMin.length==0 || this.stackMin[this.min.length-1]>=x){
        this.stackMin.push(x);
    }
};

/**
 * @return {void}
 */
MinStack.prototype.pop = function() {
    //this.stack.pop();
    //判断stack中即将弹出的元素和min栈顶的元素是否相等,若相等,则要把min栈顶的元素弹出,防止找不到最小值
    if(this.stack[this.stack.length-1]==this.min[this.stackMin.length-1]){
        this.stackMin.pop();
    }
    this.stack.pop();
};

/**
 * @return {number}
 */
MinStack.prototype.top = function() {
    return this.stack[this.stack.length-1];
};

/**
 * @return {number}
 */
MinStack.prototype.getMin = function() {
    return this.stackMin[this.stackMin.length-1];//min栈顶保存当前栈中的最小值
};

4.无序数组排序后的最大相邻差

如 2,6,3,4,5,10,9 数组中,找出最大的相邻差

思路

方案1:先把数组排序,然后一次判断两个数字间距

排序后:2,3,4,5,6,9,10

依次比较:找出是 6,9 相差 3

  1. 方案2:使用计数排序,依次放入0到10 的数组里

image.png

  1. 方案3:使用桶排序,填满不同桶后,桶内取出摆放的最大值和最小值。 然后判断两桶之间的差。用 右边的min - 左边的max = 3

5.如何用栈实现队列

思路

使用两个栈实现。

  • stack1正常记录入栈信息。 stack2 把stack1的数据pop一遍在push回stack2。
  • 只要stack一直有数据就一直pop对应数据,直到不能再pop,再把stack1新插入的数据再一次转化为stack2的数据。

image.png

image.png

let stack1 = []
let stack2 = [] 

function push(num) {
    stack1.push(num)
}

function pop() {
    if(stack1.length == 0 && stack2.length == 0) {
        return null
    } 
    if(stack2.length == 0) {
       //把stack1 转化为stack2 
         while(stack1.length) {
            stack2.push(stack.pop()) 
         }  
    } 
    return stack2.pop() 
}

6.寻找全排列的下一个数

已知:

  • 12345,则需要返回 12354
  • 12354,则需要返回 12435
  • 12435,则需要返回 12453

思路

求最大数时:逆序排序下是应该最大越好,如12345

求最小数时:顺序排序下是应该最小越好,如54321

尽量保持高位不变,低位在最小范围内变换顺序。变化顺序就是 逆序=》顺序,顺序=〉逆序。

如12345 ,要变换顺序 取决于逆序区域 5,4 =》 4,5

要接近原数并且比原数大,则必须从倒数第三位开始改变。

如 12354 -》 12453

再 12453 -》 12435

核心逻辑

1.从后向前查看逆序区域,找到逆序区域的前一位,也就是数字置换的边界。 2.让逆序区域的前一位和逆序区域中大于它的最小的数宇交换位置。 3.把原来的逆序区域转为顺序状态。 最后让我们用代码来实现一下。这里为了方便数宇位置的交换, 入参和返回值的类型都来用了整型数组。

7.删除k个数字后能组合的最小值

如 1593212

删除1个数,能得到最小值。 那应该是 删9 ,得 153212

删除3个数,能得到最小值。 那应该是 删5,9,3 ,得 1212

思路

当只删一个字时,求最小,关键是移除最高位的数

把原整数的所有数字从左到右进行比较,如果发现某一位数宇大于它右面的数字,那么在删除该数字后,必然会使该数位的值降低,因为右面比它小的数字顶替了它的位置。

如 541270936 ,依次删除顺序是 5, 4, 7

代码实现可以通过栈 优化删除数字的判断。

8.如何实现大整数的相加

js最大的浮点长度是10位,超过相加就会溢出

思路

通过把大数字,每一个数字转化成数组,然后两个数组根据下标进行合计,最终再把数组转化为字符串显示。

优化方案:可以把每一位数字直接放入9位的数字计算 ,把整个数组长度缩短

9.如何求解进矿

有一位国王拥有5座金矿,每座金矿的黄金储量不同,雳要参与挖掘的工人人数也不同。

例如有的金矿储量是500kg黄金,需要5个工人来挖掘;有的金矿储量是200kg黄金,需要3个工人来挖掘…• 如果参与挖矿的工人的总数是10。每座金矿要么全挖,要么不挖,不能派出一半人挖取一半的金矿。要求用程序求出,要想得到尽可能多的黄金,应该选择挖取哪几座金矿?

  • 总共10名工人
  • 200kg 黄金/3人
  • 300kg 黄金/4人
  • 350kg黄金/3人
  • 400kg 黄金/5人
  • 500kg黄金/5人

思路

1.贪心算法

找出性价比最高的队伍。然后排序后投入

第一名:350kg路金分人的金我,人均产值为116.6。 第一名:500kg地金5人的金矿,人均产值为100。 第一名:200kg黄金15人的金矿,人均产值80。 第一名:300kg黄金/4人的金矿,人均产值为75。 第一名:200kg黄金/3人的金矿,人均产值为66。 所以,小灰得出的最佳 金矿收益是350+500日7850kg黄金。

2.动态规划

状态转移方程式

  • 金矿数量 n , 工人数为 w
  • 金矿含金量为 数组g[], 开采人数为 p[]
  1. 当金矿数量为0,工人数为0,则有 F(n,w) = 0 (n=0 或 w= 0)

  2. 当所剩工人不够挖当前金矿,只有一个最优的子结构 F(n,w) = F(n-1,w)(n>=1,w<p[n-1])

  3. 在正常情况下,有两种最优的子结构,(要么挖 要么不挖) F(n,w) = max(F(n-1,w),F(n-1,w-p[n-1]+g[n-1] ) ) (n>=1,w>=p[n-1])

逻辑实现

  1. 递归方式 多做很多重复的判断

image.png 2. 动态规划

image.png

(1) 二维数组实现

(2) 1维数组实现 我们只保存一行数据,在计算下一行时候,要从右边往左边统计,把旧的数据一个个替换掉


/*
@param w 工人数量  
@param p [] 金矿开采所盂的工人数量 
@param g [] 金矿储量
 */
function getBestGoldMiningV3 ( w,  p, g) {
    let results = []
    for (let i = 0; i < g.length; i++) {
        for (let j = w; j >= 1; j--) {
            if(j >= p[i-1]) {
                results[j] = Math.max(results[j],results[j- p[i-1]] + g[i-1]) 
            }
        }
    }
    return results[w] 
} 

10.寻找缺失的整数

在一个无序的数组有99个不重复的正整数,范围是1-100,缺少一个1-100之间的一个数,求出这个数?

思路

  1. 创建1-100的哈希表,读到一个旧删除一个,最后剩下的就是缺失的。

  2. 把所有数字排序后,依次遍历,如何相差不是1,则找到缺失值。

  3. 先把1-100 所有数都相加。然后依次减去所有值,剩下的就是所缺失的值。

扩展

  1. 在范围1-100之间正整数的无序数组 ,有99个出现了偶次数,有一个出现了奇次数。求这个出现奇数次的值。

使用位运算的 异或

如 [3,1,3,2,4,1,4] 异或 后得到 2 就是那个奇数

  1. 在范围1-100之间正整数的无序数组 ,有98个出现了偶次数,有两个出现了奇次数。求这个两个奇数次的值。

如 [4,1,2,2,5,1,4,3], 异或 后得到 00000110B

这个结果 是。3和5 的异或结果,所以使用分治法,倒数第二位为0的为一个新数组,倒数第二位为1的为第二个新数组。得到

数组1:[4,1,5,1,4]

数组2:[2,2,3]

对于两个数组再分别进行异或,求出是 5和 3.

实际应用

1.bitmap应用

用户信息系统,包含用户多个标签:名字,性别,年龄,职业,电话。可以通过标签快速搜索指定人群。

思路

1.数据库列保存

  1. 使用数据库记录所有用户和标签信息。每次只需要执行sql,就可以查询对应标签用户。

缺点:由于标签是动态添加的,表的字段会越来越多,查询的效率也会越来越慢。

2. 使用bitmap

可以理解是一个很长很长的bit位。通过把用户id转化为下标,存在的进行标1。每一个bitmap代表不同的标签。如 性别bitmap,年龄bitmap。

1. 记录

这里下标 直接对应 用户的id,当要查询性别是男的用户。直接便利bitmap就ok了

同理:年龄也是一样做法

image.png

2. 求与

如果需要同时检查性别:男 并且 年龄 :80,只需要把两个bitmap求余即可

image.png

3. 求或

如果需要同时检查性别:男  或者 年龄 :80,只需要把两个bitmap求或 即可

image.png

4. 求非

通过提前求出所有全量用户,再进行异或得到 非的数据。

  1. 先求年龄的全量 80 + 90 image.png

  2. 求异或 得到非80 image.png

2.LRU算法的应用

定义

least recently used 最近最少使用

把hashmap的每个元素,通过双向链表的方式关联起来。

  • 最右边是最常用数据,最左边为带移除数据。
  • 每次都从右边插入,并从左边移除。
  • 每次读取数据,如果已存在,则把当前数据移动到最后
  • 每次从最左边开始删除数据。

插入

image.png

读取

image.png

删除

当超过容量限制,优先移除最前面的元素,再输入新元素 image.png

3.A星寻路算法

有效路径算法。通过全局路径节点,求解起始点到目标点的最短路径 ,如果存在最短路径,无论在什么情况之下,都能够保证找到这条最短路径。 又名:启发式算法

问题

求起点(1,2) 到终点(5,2) 的最佳路径? image.png

题解

两个集合:

  • OpenList:可到达的格子
  • CloseList:已到达的格子 一个公式:
  • F = G +H

每一个格子都具有F、G、H这三个属性

  • G: 从起点走到当前格子的成本,也就是已经花费了多少步。
  • H: 在不考虑障碍的情况下,从当前格子走到目标格子的距离,也就是离目标还有多远。
  • F: G和H的综合评估,也就是从起点到达当前格子,再从当前格子到达目标格子的总步数。

A星寻路的具体步骤

第1步,把起点放入OpenList,也就是刚才所说的可到达格子的集合。

第2步,找出OpenList中F值最小的方格作为当前方格。虽然我们没有直接计算起点方格的F值,但此时OpenList中只有唯一的方格Grid(1,2), 把当前格子移出OpenList,放入CloseList。代表这个格子已经到达并检查过了。

第三步,找出当前方格(刚刚检查过的格子)上、下、左、右所有可到达的格子,看它们是否在OpenList或CloseList当中。如果不在,则将它们加入OpenList,计算出相应的G,H,F值,并把当前格子作为它们的"父节点"。

通过不断判断F(一般以F最大的优先)来新增或移除 openList 和colseList两个集合。直到openList出现终点(5,2),则寻路结束。

4.红包分配算法实现。

如何把100元红包随机分配给5个人。 要求:

  • 所有金额之和要等于总金额
  • 每个人最少有一分钱
  • 红包要尽可能均匀,不能差异化太大

实现

1. 使用随机数切割

每次拆分金额 = 随机区间[1分, 剩余金额-1分]

每一次的金额都是基于上一次的剩余

如:

  • 第1次: [0.01, 99.99] ,中50
  • 第2次: [0.01, 50] , 中20
  • 第3次: [20, 50]

后面可以随机的金额就越来越小。

2. 使用二倍均值法

m为红包总金额,n为剩余人数

每次抢到红包 = 随机区间[0.01, m/n*2 -0.01]

每一次的金额都是基于上一次的剩余

  • 第1次: 100 /5 * 2 = 40, [0.01, 39.99] ,中20,剩80
  • 第2次: 80 /4 * 2 = 40, [0.01, 39.99] ,中20
  • 第3次: 60 /3 * 2 = 40, [0.01, 39.99] ,中20 ,当然并不能保证每次都是中间值20,但是上浮动会保持在一定范围内。

3. 线段切割法

image.png 把红包的总金额看成一条很长的线段,而且每个人抢到的金额,则主线段拆分出来的子线段。

最开始就使用随机函数生成多个区间段的点。

当n个人抢红包时候,就需要确定n-1个切割点。做n-1次随机运算。

需要注意两点:

  • 当随机切割点出现重复,要特殊处理
  • 如何尽量降低时间和空间的复杂度