FT二

2,638 阅读21分钟

1、逻辑题:如果你有两个桶,一个装红颜料,另一个装蓝颜料,两个桶的颜料一样多。你从蓝颜料里舀一杯,倒入红色颜料桶,再从红色颜料桶里舀一杯倒入蓝色颜料桶,假设红桶中的红色/蓝色 为A,蓝桶中的蓝色/红色 为B,A和B哪个大?

答案:一样大,设两桶原本各有x升颜料,一杯是y升颜料,

  • 第一步,从蓝颜料桶舀一杯(即y升)到红颜料桶,此时红颜料桶有红颜料 x升,蓝颜料 y升,红色/蓝色 = x/y;
  • 第二步, 从红颜料桶舀一杯(即y升)到蓝颜料桶,由第一步可知,红颜料在红桶占比为:x/(x+y),蓝颜料占比为:y/(x+y),则,在舀到蓝桶的y升颜料中,红颜料占:y * (x/(x+y)) ,蓝颜料占:y * (y/(x+y))那在蓝桶中,红颜料有 y * (x/(x+y)) 升,蓝颜料:(x-y)+y * (y/(x+y))升,化简可得:蓝/红 = x/y

2、设计题:抽奖设计,假设双十一当天,公司要做一个注册拉新的活动,每个来注册的新用户都有一次抽奖机会,抽奖的方式为大转盘的方式,具体前端页面如下图,设计一个关于抽奖的前后端交互接口,需要列出主要字段,尽可能详细。

![图片的标注](图片链接地址)

//这道题因人而异,这一年我是纯前端,接口都是后端直接提供的,所以面试的时候答的不是很好 //面试完请教了一下后端小哥哥 注册拉新,一次机会,一个接口完成抽奖流程。 三个功能: 1.初始化的奖品的数据 2.注册之后的用户数据 3.用户抽奖 1,2当它有,只设计抽奖交互接口: 服务器完成抽奖返回结果,参数需要用户信息,结果返回抽奖结果

get:/{userId}
response:
{
"code":0,//未开始,已结束,未登录,用户抽奖机会已用尽,成功
"msg":"code描述信息",
"data":{
"jpInfo":{
"id":,price,picUrl,name,level//这里如果初始化中有奖品了,只需要ID
},
}
}

一个抽奖程序的前后端逻辑 回答的过程中一直有和面试官讨论我的想法和出发点,最后回答了以下几点: 奖品占比概率,奖品份数,假设抽奖总人数 前后端分工,安全问题,是否由后台进行抽奖计算 考虑奖品抽完的情况

程序设计题: 思路:在后台实现抽奖函数,先random判断用户是否中奖,在list中存储中奖的节点,一等奖为1,二等奖为2,第二次random判断用户中间的大小。用户中奖率为N:M,N为总奖品数,M为总用户数。

3、个税题: 红标为我的答案,做这题有点崩溃,可能是前面答的不好导致太紧张,面的时候死活理不清面试结束2分钟就想出来了~有点蓝瘦~

4、语言题:如果要实现一个聊天室的逻辑,有room和user两个类,而room设置有一个门,而且有“主人的设定,只有主人才能开门和关门。请问开门和关门的方法,你会放到room类还是user类中实现 ?为什么?下图的两种实现方法,哪个更好?

//我的回答是 Room中更好,可以把开门关门放在prototype,开关门作为自身属性,这回答估计不是很好,面试官不是很满意,但我不大清楚其他的了,一年的小菜鸟,水平有限。

多态的实际含义是:同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结 果。换句话说,给不同的对象发送同一个消息的时候,这些对象会根据这个消息分别给出不同的 反馈。

5、一个整数的数组如下:

int a[]={21,11,45,56,9,66,77,89,78,68,100,120,111} 请查询数组中有没有比它前面元素都大,比它后面的元素都小的数,没有打印-1,有显示其索引。要求时间复杂度和空间复杂度最大都是O(N)。一个数组存从左到右的最大值max,另一个数组存从右到左的最小值min


 const getPrintThePivotElements = (data, len) => {
      //从右往左,寻找每个位置及其之后的最小数
      const rightMin = []
      let r_min = data[len - 1]
      for (let i = len - 1; i >= 0; --i) {
        if (data[i] < r_min) {
          r_min = data[i]
          rightMin[i] = r_min
        }
      }
      console.log("rightMin", rightMin)

      //从左往右,寻找比左边大且比右边小的数
      let l_max = data[0]
      const index =[]
      for (let i = 0; i < len - 1; ++i) {
        if (data[i] > l_max) {
          l_max = data[i]
          if (data[i] === rightMin[i]) {
            index.push(i)
          }
        }
      }

      console.log("index", index)
      return index
    }
    const arr = [21, 11, 45, 56, 9, 66, 77, 89, 78, 68, 100, 120, 111]
    getPrintThePivotElements(arr, arr.length)

7、洗牌算法:

let swap = a => {
  let b = [];
  for(var i = a.length;i > 0;) {
        var index = Math.floor(Math.random() * i);
    b.push(a[index]);
    a[index] = a[--i];
  }
  return b;
}

8、实现一个fibonacci函数,输入数字n,输出fibonacci数列的第n项数字,并给该函数加入缓存功能。

这里用动态规划来实现会简单一些,但是题目要求有缓存功能

function fibonacci(n, map = {}){
if(n<=1){
map[n] = n;
return n;
}
if(!map[n]){
map[n] = fibonacci(n-1, map) + fibonacci(n-2, map)
}
return map[n];
}


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

9、写个new

首先我们再来回顾下 new 操作符的几个作用

new 操作符会返回一个对象,所以我们需要在内部创建一个对象 这个对象,也就是构造函数中的 this,可以访问到挂载在 this 上的任意属性 这个对象可以访问到构造函数原型上的属性,所以需要将对象与构造函数链接起来 返回原始值需要忽略,返回对象需要正常处理

回顾了这些作用,我们就可以着手来实现功能了

function create(Con, ...args) {
  let obj = {}
  Object.setPrototypeOf(obj, Con.prototype)
  let result = Con.apply(obj, args)
  return result instanceof Object ? result : obj
}

复制代码这就是一个完整的实现代码,我们通过以下几个步骤实现了它:

首先函数接受不定量的参数,第一个参数为构造函数,接下来的参数被构造函数使用 然后内部创建一个空对象 obj 因为 obj 对象需要访问到构造函数原型链上的属性,所以我们通过 setPrototypeOf 将两者联系起来。这段代码等同于 obj.proto = Con.prototype 将 obj 绑定到构造函数上,并且传入剩余的参数 判断构造函数返回值是否为对象,如果为对象就使用构造函数返回的值,否则使用 obj,这样就实现了忽略构造函数返回的原始值

接下来我们来使用下该函数,看看行为是否和 new 操作符一致

function Test(name, age) {
  this.name = name
  this.age = age
}
Test.prototype.sayName = function () {
    console.log(this.name)
}
const a = create(Test, 'yck', 26)
console.log(a.name) // 'yck'
console.log(a.age) // 26
a.sayName() // 'yck'

10、给定一个字符串里面只有"R" "G" "B" 三个字符,请排序,最终结果的顺序是R在前 G中 B在后。 要求:空间复杂度是O(1),且只能遍历一次字符串。

  • 对于一个合法字符串(即R在前 G中 B在后),如RRGGBB

  • 在这个字符串后面再加个R,变成 RRGGBBR,如要把它变成合法字符串,先跟 第一个出现的G 交换

  • 字符串变成   RRRGBBG  ,再把 末尾的G  跟 第一个出现的B 交换

字符串变成  RRRGGBB。

  • 如果是 在这个字符串后面 加一个 G ,RRGGBBG,则只需要把  G 与 第一个出现的 B 交换,字符串变成  RRGGGBB

  • 如果是 在这个字符串后面 加一个B,则无需操作。

void swapChar(char *s, int i, int j)
{
    char temp = s[i];
    s[i] = s[j];
    s[j] = temp;
}

/**
 * 划分函数
 */
void partition(char *s, int lo, int hi, char t)
{
    int m = lo-1, i;
    for (i = lo; i <= hi; i++) {
        if (s[i] != t) {
            swapChar(s, ++m ,i);
        }
    }
}
 
/**
 * RGB排序-遍历两次
 */
void rgbSortTwice(char *s)
{ 
    int len = strlen(s);
    partition(s, 0, len-1, 'G');  // 以G划分,划分完为 RBBRBBGGGG
    partition(s, 0, len-1, 'B');  // 再以B划分,划分完为 RRGGGGBBBB
}

1)如果第i个位置为字符R,则与前面的指示变量r的后一个字符也就是++r处的字符交换,并++g,此时还需要判断交换后的i里面存储的字符是否是G,如果是G,则需要将其与g处的字符交换;

2)如果第i个位置为字符G,则将其与++g处的字符交换即可。++g指向的总是下一个应该交换G的位置,++r指向的是下一个需要交换R的位置。

3)如果第i个位置为字符B,则什么都不做,继续遍历。


const swap = (arr, a, b) => {
  let c = arr[a]
  arr[a] = arr[b]
  arr[b] = c
}

const rgbSort = (str) => {
  const strArr = [...str]
  let r = -1, g = -1
  for (let i = 0; i < strArr.length; i++) {
    if (str[i] === "R") {
      swap(strArr, ++r, i);
      ++g;
      if (strArr[i] === "G") {
        swap(strArr, g, i)
      }
    } else if (strArr[i] === "G") {
      swap(strArr, ++g, i)
    }
  }
  return strArr.join("")
}


rgbSort("RGBBGGRR")

11、快速排序:

let quickSort = arr => {
    if (arr.length <= 1) {
        return arr;
    }
    let pivotIndex = Math.floor((arr.length - 1) / 2);
    let pivot = arr.splice(pivotIndex, 1)[0];
    let left = [];
    let right = [];
    for (let i of Object.keys(arr)) {
        let v = arr[i];
        if (v <= pivot) {
            left.push(v);
        } else {
            right.push(v);
        }
    }
    return quickSort(left).concat([pivot], quickSort(right));

function quickSort(arr, begin, end) {
    //递归出口
    if(begin >= end)
        return;
    var l = begin; // 左指针
    var r = end; //右指针
    var temp = arr[begin]; //基准数,这里取数组第一个数
    //左右指针相遇的时候退出扫描循环
    while(l < r) {
        //右指针从右向左扫描,碰到第一个小于基准数的时候停住
        while(l < r && arr[r] >= temp)
            r --;
        //左指针从左向右扫描,碰到第一个大于基准数的时候停住
        while(l < r && arr[l] <= temp)
            l ++;
        //交换左右指针所停位置的数
        [arr[l], arr[r]] = [arr[r], arr[l]];
    }
    //最后交换基准数与指针相遇位置的数
    [arr[begin], arr[l]] = [arr[l], arr[begin]];
    //递归处理左右数组
    quickSort(arr, begin, l - 1);
    quickSort(arr, l + 1, end);
}

var arr = [2,3,4,1,5,6]
quickSort(arr, 0, 5);
console.log(arr)

12、冒泡排序的实现,复杂度以及优化

答: 优化点:做swap标记,若循环下来没有交换过则说明已经排序完成 复杂度 最优n 平均n2 最差n2

快速排序和冒泡排序的稳定性问题

  • 稳定:如果 a 原本在 b 前面,而 a=b,排序之后 a 仍然在 b 的前面。
  • 不稳定:如果 a 原本在 b 的前面,而 a=b,排序之后 a 可能会出现在 b 的后面。
// 冒泡排序
var arr = [3, 4, 1, 2];
function bubbleSort (arr) {
 var max = arr.length - 1;
 for (var j = 0; j < max; j++) {
 // 声明⼀个变量,作为标志位
 var done = true;
 for (var i = 0; i < max - j; i++) {
 if (arr[i] > arr[i + 1]) {
 var temp = arr[i];
 arr[i] = arr[i + 1];
 arr[i + 1] = temp;
 done = false;
 }
 }
 if (done) {
 break;
 }
 }
 return arr;
}
bubbleSort(arr);

20、爬楼梯算法

3个台阶:
n-2  + n-1
n-1  11  2
n-2 1
111 12 21

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

13 请使用一个长度为n的数组,实现一个循坏队列,写出主要函数实现(入队列,出队列,判断是否为满、是否为空),编程语言不限。

方法一:数组 思路

根据问题描述,该问题使用的数据结构应该是首尾相连的 环。

任何数据结构中都不存在环形结构,但是可以使用一维 数组 模拟,通过操作数组的索引构建一个 虚拟 的环。很多复杂数据结构都可以通过数组实现。

对于一个固定大小的数组,任何位置都可以是队首,只要知道队列长度,就可以根据下面公式计算出队尾位置:

tailIndex=(headIndex+count−1)modcapacity

其中 capacity 是数组长度,count 是队列长度,headIndex 和 tailIndex 分别是队首 head 和队尾 tail 索引。下图展示了使用数组实现循环的队列的例子。

avatar

算法

设计数据结构的关键是如何设计 属性,好的设计属性数量更少。

属性数量少说明属性之间冗余更低。

属性冗余度越低,操作逻辑越简单,发生错误的可能性更低。

属性数量少,使用的空间也少,操作性能更高。

*但是,也不建议使用最少的属性数量。*一定的冗余可以降低操作的时间复杂度,达到时间复杂度和空间复杂度的相对平衡。

根据以上原则,列举循环队列的每个属性,并解释其含义。

queue:一个固定大小的数组,用于保存循环队列的元素。

headIndex:一个整数,保存队首 head 的索引。

count:循环队列当前的长度,即循环队列中的元素数量。使用 hadIndex 和 count 可以计算出队尾元素的索引,因此不需要队尾属性。

capacity:循环队列的容量,即队列中最多可以容纳的元素数量。该属性不是必需的,因为队列容量可以通过数组属性得到,但是由于该属性经常使用,所以我们选择保留它。这样可以不用在 Python 中每次调用 len(queue) 中获取容量。但是在 Java 中通过 queue.length 获取容量更加高效。为了保持一致性,在两种方案中都保留该属性。


// queue:一个固定大小的数组,用于保存循环队列的元素。

// headIndex:一个整数,保存队首 head 的索引。

// count:循环队列当前的长度,即循环队列中的元素数量。使用 hadIndex 和 count 可以计算出队尾元素的索引,因此不需要队尾属性。

// capacity:循环队列的容量,即队列中最多可以容纳的元素数量。该属性不是必需的,因为队列容量可以通过数组属性得到,但是由于该属性经常使用,所以我们选择保留它。这样可以不用在 Python 中每次调用 len(queue) 中获取容量。但是在 Java 中通过 queue.length 获取容量更加高效。为了保持一致性,在两种方案中都保留该属性。


class MyCircularQueue {
  constructor(length) {
    this.queue = new Array(length).fill(0)
    this.headIndex = 0
    this.count = 0
    this.capacity = length
  }

  enQueue(val) {
    if (this.count === this.capacity) {
      return false
    }
    this.queue[(this.headIndex + this.count) % this.capacity] = val
    this.count += 1
    return true
  }

  deQueue() {
    if (this.count == 0)
      return false;
    this.headIndex = (this.headIndex + 1) % this.capacity;
    this.count -= 1;
    return true;
  }

  front() {
    if (this.count === 0) return -1
    return this.queue[this.headIndex]
  }

  rear() {
    if (this.count) return -1;
    let tailIndex = (this.headIndex + this.count - 1) % this.capacity
    return this.queue[tailIndex]
  }

  isEmpty() {
    return !!this.count
  }

  ifFull() {
    return this.count === this.capacity
  }
}

13、1000个找最大50个数

// 交换两个节点
function swap(A, i, j) {
 let temp = A[i];
 A[i] = A[j];
 A[j] = temp;
}
// 将 i 结点以下的堆整理为⼤顶堆,注意这⼀步实现的基础实际上是:
// 假设 结点 i 以下的⼦堆已经是⼀个⼤顶堆,shiftDown函数实现的
// 功能是实际上是:找到 结点 i 在包括结点 i 的堆中的正确位置。后⾯
// 将写⼀个 for 循环,从第⼀个⾮叶⼦结点开始,对每⼀个⾮叶⼦结点
// 都执⾏ shiftDown操作,所以就满⾜了结点 i 以下的⼦堆已经是⼀⼤
//顶堆
function shiftDown(A, i, length) {
 let temp = A[i]; // 当前⽗节点
// j<length 的⽬的是对结点 i 以下的结点全部做顺序调整
 for(let j = 2*i+1; j<length; j = 2*j+1) {
     temp = A[i]; // 将 A[i] 取出,整个过程相当于找到 A[i] 应处于的位置
     if(j+1 < length && A[j] < A[j+1]) {
     j++; // 找到两个孩⼦中较⼤的⼀个,再与⽗节点⽐较
     }
     if(temp < A[j]) {
     swap(A, i, j) // 如果⽗节点⼩于⼦节点:交换;否则跳出
     i = j; // 交换后,temp 的下标变为 j
 } else {
 break;
 }
 }
}
// 堆排序
function heapSort(A) {
 // 初始化⼤顶堆,从第⼀个⾮叶⼦结点开始
 for(let i = Math.floor(A.length/2-1); i>=0; i--) {
 shiftDown(A, i, A.length);
 }
 // 排序,每⼀次for循环找出⼀个当前最⼤值,数组⻓度减⼀
 for(let i = Math.floor(A.length-1); i>0; i--) {
 swap(A, 0, i); // 根节点与最后⼀个节点交换
 shiftDown(A, 0, i); // 从根节点开始调整,并且最后⼀个结点已经为当
 // 前最⼤值,不需要再参与⽐较,所以第三个参数
 // 为 i,即⽐较到最后⼀个结点前⼀个即可
 }
}
let Arr = [4, 6, 8, 5, 9, 1, 2, 5, 3, 2];
heapSort(Arr);

15、将一个分数转化成小数形式输出,因为分数只可能是有限小数或无限循环小数。如果小数是无限循环小数,则使用小括号括起

// 重复余数则为循环
/**
 * @param {number} numerator
 * @param {number} denominator
 * @return {string}
 */
var fractionToDecimal = function(numerator, denominator) {
    if (!numerator) return "0";
    var fraction = [], remain = 0, map = new Map();
    if (numerator < 0 ^ denominator < 0) {
        fraction.push("-");
    }
    num = Math.abs(numerator);
    deno = Math.abs(denominator);
    //整数部分
    fraction.push(Math.floor(num / deno).toString());
    remain = num % deno;
    if (remain) {
        fraction.push(".");
    }
    while (remain) {
        //存在重复余数则必出现循环
        if (map.has(remain)) {
            //插入左括号
            fraction.splice(map.get(remain), 0, "(");
            fraction.push(")");
            break;
        }
        map.set(remain, fraction.length);
        remain *= 10;
        fraction.push(Math.floor(remain / deno).toString());
        remain %= deno;
    }
    return fraction.join("");
};

17、给一篮子鸡蛋,如何用最少的比对次数找到最大的和最小的鸡蛋

我的回答是 一开始,随便拿出两个鸡蛋,然后接下来剩下的鸡蛋每拿出一个,就和这两个进行比较,如果比两个中小的鸡蛋小就替换小的那个,如果比大的鸡蛋大就替换掉大的那个,这样比较次数就是 (n-2)*2 ,因为有些鸡蛋只用比一次就行了(因为是最小的就不用往大的比了),所以结果是小于 (n-2)*2的,但我觉得应该还可以更加快....

比鸡蛋的题可以采用分组的方法。假如有八个蛋,通过四次比较,可分成大蛋组和小蛋组,每组各四个。对于大蛋组中的四个,两两比较两次可以得到含两个蛋的超大组,最后比较一次得到最大蛋,得到最小蛋的过程同理。 总计需要4+2+2+1+1=10次。

18、排序二叉树的插入,如果插入相同的数,需要做什么保证树可以还原(即不允许丢弃到相同的数

19、

20、Http是在哪一层,Https使用了非对称加密还是对称加密?TCP和UDP的区别,为什么UDP不可靠还要使用UDP

21 .排序算法简述(常用的冒泡插入选择堆快排归并)说流程,说时间空间复杂度,说改进

21.二叉树的理解(二叉搜索树,AVL树,B树,B+树)

22、删除单链表的倒数第N个节点,返回新链表(剑指offer原题,快慢指针)——不过我用了一个for和一个while,可以更简化用一个while就好,当时也是让我优化,我一时没反应过来。。。。

方法一:两次遍历算法 思路

我们注意到这个问题可以容易地简化成另一个问题:删除从列表开头数起的第 (L - n + 1)(L−n+1) 个结点,其中 LL 是列表的长度。只要我们找到列表的长度 LL,这个问题就很容易解决。

算法

首先我们将添加一个哑结点作为辅助,该结点位于列表头部。哑结点用来简化某些极端情况,例如列表中只含有一个结点,或需要删除列表的头部。在第一次遍历中,我们找出列表的长度 LL。然后设置一个指向哑结点的指针,并移动它遍历列表,直至它到达第 (L - n)(L−n) 个结点那里。我们把第 (L - n)(L−n) 个结点的 next 指针重新链接至第 (L - n + 2)(L−n+2) 个结点,完成这个算法。

图 1. 删除列表中的第 L - n + 1 个元素

Java

public ListNode removeNthFromEnd(ListNode head, int n) {
    ListNode dummy = new ListNode(0);
    dummy.next = head;
    int length  = 0;
    ListNode first = head;
    while (first != null) {
        length++;
        first = first.next;
    }
    length -= n;
    first = dummy;
    while (length > 0) {
        length--;
        first = first.next;
    }
    first.next = first.next.next;
    return dummy.next;
}
  • 实现单链表
/**
 *  @desc  单向链表
 *  @author CC-Cai
 */


class Node {  // 链表节点
  constructor(element) {
    this.element = element;
    this.next = null;   // 节点的指向下个节点的指针
  }
}


class NodeList {   //  链表
  constructor(item) {
    this.head = new Node(item);   //  初始化链表的头节点
  }
  /**
   * @description 插入元素
   * @param {需要插入的元素} newItem 
   * @param {插入到某一元素之后} beforeItem 
   */
  insertInNext(newItem, beforeItem) {
    let newNode = new Node(newItem);
    if (beforeItem) { //  判读是否是插入到指定节点后面,如果不是则插入到最后一个节点。
      let currNode = this.find(beforeItem);
      newNode.next = currNode.next;
      currNode.next = newNode;
    } else {
      let lastNode = this.findLastNode();
      lastNode.next = newNode;
    }
  }
  /**
   * @description 删除元素
   * @param {删除的元素} newItem 
   */
  remove(item) {
    let preNode = this.findPreNode(item);  //  找到前一节点,将前一节点的next指向该节点的next
    if (preNode.next != null) {
      preNode.next = preNode.next.next;
    }
  }
  /**
   * @description 查找元素的节点
   * @param {查找的元素} item 
   */
  find(item) { //  根据元素查找节点
    let currNode = this.head;
    while (currNode.element !== item && currNode) {
      if (currNode.next) {
        currNode = currNode.next;
      } else {
        currNode = null;
      }
    }
    return currNode;
  }
  /**
   * @description 查找最后一个节点
   */
  findLastNode() {
    let currNode = this.head;
    while (currNode.next) {
      currNode = currNode.next;
    }
    return currNode;
  }
  /**
   * @description 查找元素的前一节点
   * @param {查找的元素} item 
   */
  findPreNode(item) {
    let currNode = this.head;
    while (currNode && currNode.next && currNode.next.element !== item) {
      if (currNode.next) {
        currNode = currNode.next;
      } else {
        currNode = null;
      }
    }
    return currNode;
  }
  toString() {
    let currNode = this.head;
    let strList = [];
    while (currNode.next) {
      strList.push(JSON.stringify(currNode.element));
      currNode = currNode.next;
    }
    strList.push(JSON.stringify(currNode.element));
    return strList.join('->')
  }
}


let ming = { name: '小明', score: 100 },
  hongs = { name: '小红', score: 88 },
  jun = { name: '小军', score: 66 },
  li = { name: '小李', score: 50 };

let nList = new NodeList(ming);

nList.insertInNext(hongs);
nList.insertInNext(li);
nList.insertInNext(jun, hongs);

console.log('' + nList);

nList.remove(hongs);

console.log('' + nList);

/*
分析问题: 1->2->3->4->5
1. 删除头部, n = 5
2. 删除尾部, n = 1
3. 删除中间, n = 2
思路:
1. 快慢指针,删除头部,边界处理问题极烦,增加一个node
2. 删除尾部和删除中间画图,你会发现一样。
核心:
1. 找到要删除的节点,将其前一个节点,指向要删除节点的后一个节点
*/
var removeNthFromEnd = function(head, n) {
// 边界处理
if(n <= 0 || !head) return head;

let node = new ListNode(-1);
node.next = head;

let count = 0,
    fast = node;

// 快指针先走n步
while(count < n) {
    fast = fast.next;
    count++;
}

let slow = node,
    prev = null;

// 快慢指针并行前进
while(fast) {
    fast = fast.next;
    prev = slow;
    slow = slow.next;
}

// 这里面不存在slow为null的情况, 因为slow为null的时候, n为0, 在上面已经做了边界处理
prev.next = slow.next;
return node.next;
};

复杂度分析

时间复杂度:O(L)O(L),该算法对列表进行了两次遍历,首先计算了列表的长度 LL 其次找到第 (L - n)(L−n) 个结点。 操作执行了 2L-n2L−n 步,时间复杂度为 O(L)O(L)。

空间复杂度:O(1)O(1),我们只用了常量级的额外空间。

23.智力题 一家3个孩子,已知一个男孩,剩下至少有一个女孩的概率?(@冠状病毒biss 老兄的总结有类似题)不过我最后没回答对,有思路,面试官就告诉我之后再研究吧

3/4 1-1/4

24.智力题 随机数生成器,只生成0,1,但是不公平,做一个公平的方案?(@冠状病毒biss 老兄的总结有类似题)01为0,10为1,其余重来。一开始想多了,后来简化了。

25、给个数字n,0到n每个数字的二进制的1的个数 优化到O(n)

26、给了一个图片,如何提取出图片中的数字信息,不能使用库包等

27、16个球,有一个异常球,有一个天平秤,称重多少次,可以找到那个球(称重次数尽量少)

28、足够多的水和两个水杯(7L和17L),如何得到9L水;

7%17
7
14%17
14
21%17
4
28%17
11
35%17
1
42%17
8
49%17
15
56%17
5
63%17
12
70%17
2
77%17
9

29、判断二叉树是否是镜像对称的,只需要返回true或false就行

后面才算想出来翻转左子树和右子树,然后和原来的比较。面试官就说,既然想到这了,何不把整棵树翻转再和原来的二叉树比较呢

30. 实现一个生成3~7区间的随机整数的函数

Math.floor(Math.random()*(max-min+1)+min);

31. 大医院每天接生100人,小医院每天接生50人,哪个医院能达到“生男孩几率大于60%”的目标的多一点?

Math.pow(6/10,100)<Math.pow(6/10,50)