面试笔记:面经-腾讯-一面


一、自我介绍

个人背景、项目经历、实习经历。


二、Java后台

2.1 自定义view

  1. 选择和设置构造方法;
  2. 重写onMeasure()方法;
  3. 重写onLayout()方法;
  4. 重写onDraw()方法;
  5. 自定义属性;
  6. 重写其他事件的方法(滑动监听等)。

2.2 onMeasure()方法、onDraw()方法、onLayout()方法的作用

  • onMeasure()方法:主要负责测量,决定控件本身或其子控件所占的宽高。可以通过onMeasure()方法提供的参数widthMeasureSpecheightMeasureSpec来分别获取控件宽度和高度的测量模式和测量值(测量 = 测量模式 + 测量值)。
  • onLayout()方法:负责布局,大多数情况是在自定义ViewGroup中才会重写,主要用来确定子View在这个布局空间中的摆放位置。 onLayout(boolean changed, int left, int top, int right, int bottom)方法有5个参数,其中changed表示这个控件是否有了新的尺寸或位置;lefttoprightbottom分别表示这个View相对于父布局的左/上/右/下方的位置。
  • onDraw()方法:负责绘制,即如果在Android原生控件中没有现成的支持,就需要自己绘制自定义控件的显示效果。在onDraw()方法中使用最多的两个类:PaintCanvas

2.3 快速排序

快速排序使用分治策略来把一个序列分为两个子序列。步骤为:

  1. 从数列中挑出一个元素,称为基准(pivot)
  2. 重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作
  3. 递归地把小于基准值元素的子数列和大于基准值元素的子数列排序。

递归到最底部时,数列的大小是零或一,也就是已经排序好了。这个算法一定会结束,因为在每次的迭代中,它至少会把一个元素摆到它最后的位置去。

2.4 进程和线程

  • 进程:指在系统中正在运行的一个应用程序;程序一旦运行就是进程;或者更专业化来说:进程是指程序执行时的一个实例,即它是程序已经执行到各种程度的数据结构的汇集。从内核的观点看,进程的目的就是担当分配系统资源(CPU时间、内存等)的基本单位。
  • 线程:系统分配处理器时间资源的基本单元,或者说进程内独立执行的一个单元执行流。进程是资源分配的最小单位,线程是程序执行的最小单位。

2.5 堆内存和栈内存

  • 堆内存:为成员分配和释放,由程序员自己申请、自己释放。否则发生内存泄露。典型为使用new申请的堆内容。
    栈内存:为编译器自动分配和释放,如函数参数、局部变量、临时变量等等。
  • 堆内存:在Java运行时被使用来为对象和JRE类分配内存。不论什么时候创建了对象,它将一直会在堆内存上创建。垃圾回收运行在堆内存上来释放没有任何引用的对象所占的内存,任何在堆上被创建的对象都有一个全局的访问,并且可以在应用的任何位置被引用。
  • 栈内存:被用来线程的执行,包含生命周期很短的具体值的方法和在堆中使用这个方法对象的引用。栈内存是LIFO(后进先出)序列。当方法被调用的时候,堆内存中一个新的块被创建,保存了本地原始值和在方法中对其他对象的引用。这个方法结束之后,这个块对其他方法就变为可用。栈内存与堆内存相比非常小。

2.6 GC机制

  • 如何确定垃圾
    (1)引用计数法:通过引用计数来判断一个对象是否可以回收。即一个对象如果没有任何与之关联的引用, 即他们的引用计数都不为0,则说明对象不太可能再被用到,那么这个对象就是可回收对象。
    (2)可达性分析:为了解决引用计数法的循环引用问题,Java使用了可达性分析的方法。通过一系列的GC roots对象作为起点搜索。如果在GC roots和一个对象之间没有可达路径,则称该对象是不可达的。不可达对象经过两次标记为可回收对象,则将面临回收。
  • 垃圾收集算法
    (1)标记清除算法(Mark-Sweep):分为两个阶段,标记阶段标记需要回收的对象,清除阶段回收对象占用的空间。
    (2)复制算法(copying):按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉。
    (3)标记整理算法(Mark-Compact):标记阶段和Mark-Sweep算法相同, 标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。
    (4)分代收集算法:新生代采用复制算法、老年代采用标记复制算法。

2.7 内存泄漏及如何避免内存泄漏

  • 内存泄漏
    当一个对象已经不需要再使用,本该被回收时,另外一个正在使用的对象持有它的引用从而导致它不能被回收,这导致本该被回收的对象不能被回收而停留在堆内存中,这就产生了内存泄漏。
  • 内存泄露的发生场景
    (1)集合类。集合类仅仅有添加元素的方法,而没有相应的删除机制,导致内存被占用。而如果这个集合类是全局性的变量(比如类中的静态属性,全局性的map等即有静态引用或final一直指向它),那么没有相应的删除机制,很可能导致集合所占用的内存只增不减。
    (2)单例模式。不正确使用单例模式是引起内存泄露的一个常见问题,单例对象在被初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部对象的引用,那么这个外部对象将不能被jvm正常回收,导致内存泄露。
  • 避免内存泄漏
    (1)尽早释放无用对象的引用
    在使用临时变量时,让引用变量在推出活动域后自动设置为null,暗示垃圾收集器来收集该对象,防止发生内存泄漏。
    (2)程序进行字符串处理时,尽量避免使用String,而应该使用StringBuffer
    因为String类是不可变的,每一个String对象都会独立占用内存一块区域。
    (3)尽量少用静态变量
    因为静态变量是全局的,存在方法区,GC不会回收。(用永久代实现的方法区,垃圾回收行为在这个区域是比较少出现的,垃圾回收器的主要目标是针对常量池和类型的卸载)。
    (4)避免集中创建对象,尤其是大对象,如果可以的话尽量使用流操作
    JVM会突然需要大量内存,这时会触发GC优化系统内存环境。
    (5)尽量运用对象池技术以提高系统性能
    生命周期长的对象拥有生命周期短的对象时容易引发内存泄漏,例如大集合对象拥有大数据量的业务对象的时候,可以考虑分块进行处理,然后解决一块释放一块的策略。
    (6)不要在经常调用的方法中创建对象,尤其忌讳在循环中创建对象
    可以适当的使用hashtable,vector创建一组对象容器,然后从容器中去取这些对象,而不用每次new之后又丢弃。
    (7)优化配置

三、算法题(口述)

3.1 洗牌算法

  • Fisher-Yates Shuffle【时间复杂度为O(n2),空间复杂度为O(n)】
    基本思想就是从原始数组中随机取一个之前没取过的数字到新的数组中,具体如下:
    (1)初始化原始数组和新数组,原始数组长度为n(已知);
    (2)从还没处理的数组(假如还剩k个)中,随机产生一个[0, k)之间的数字p(假设数组从0开始);
    (3)从剩下的k个数中把第p个数取出;
    (4)重复步骤2和3直到数字全部取完;
    (5)从步骤3取出的数字序列便是一个打乱了的数列。
  • Knuth-Durstenfeld Shuffle【时间复杂度为O(n),空间复杂度为O(1),缺点必须知道数组长度n】
    算法步骤为:
    (1)建立一个数组大小为 n 的数组 arr,分别存放 1 到 n 的数值;
    (2)生成一个从 0 到 n - 1 的随机数 x;
    (3)输出 arr 下标为 x 的数值,即为第一个随机数;
    (4)将 arr 的尾元素和下标为 x 的元素互换;
    (5)同2,生成一个从 0 到 n - 2 的随机数 x;
    (6)输出 arr 下标为 x 的数值,为第二个随机数;
    (7)将 arr 的倒数第二个元素和下标为 x 的元素互换;
    ……如上,直到输出 m 个数为止。
  • Inside-Out Algorithm【 时间复杂度为O(n),空间复杂度为O(n) 】
    基本思思是从前向后扫描数据,把位置i的数据随机插入到前i个(包括第i个)位置中(假设为k),这个操作是在新数组中进行,然后把原始数据中位置k的数字替换新数组位置i的数字。其实效果相当于新数组中位置k和位置i的数字进行交互。
  • 参考博客
    (1)https://blog.csdn.net/qq_26399665/article/details/79831490
    (2)https://blog.csdn.net/georas/article/details/37594833

3.2 大数乘法

  • 模拟手工计算过程:逐位相乘,错位累加,最后进位。复杂度为O(n2)
public static String multiply(String num1, String num2) {
    if (num1.equals("0") || num2.equals("0"))
        return "0";
    int length1 = num1.length();
    int length2 = num2.length();
    StringBuilder sb = new StringBuilder();
    int[] array = new int[length1 + length2];
    //第一位数
    for (int i = length1 - 1; i >= 0; i--) {
        //第二位数
        for (int j = length2 - 1; j >= 0; j--) {
            int number1 = num1.charAt(i) - '0';
            int number2 = num2.charAt(j) - '0';
            array[i + j] += number1 * number2;
            if (array[i + j] >= 10 && (i + j) != 0) {
                array[i + j - 1] += array[i + j] / 10;
                array[i + j] = array[i + j] % 10;
            }
        }
    }
    // 去掉前导0
    int i = 0;
    while (i < array.length - 1 && array[i] == 0)
        i++;
    for (; i < array.length - 1; i++) {
        sb.append(array[i]);
    }
    return sb.toString();
}
  • 采用分治的思想
// len为num1、num2的长度最大值
public static String multiply(String num1, String num2, int len) {
    String str = "";
    //len 少于4位数,可直接计算
    if (len <= SIZE) { // 少于4位数,可直接计算
        return "" + (Integer.parseInt(num1) * Integer.parseInt(num2));
    }
    // 长度不同,调用formatNumber方法,补齐X、Y,使之长度相同
    if (num1.length() != num2.length()) { // 长度不同,调用formatNumber方法,补齐X、Y,使之长度相同
        num1 = formatNumber(num1, len);
        num2 = formatNumber(num2, len);
    }
    // 将num1对半分成两部分、num2对半分成两部分
    int len1 = len / 2;
    int len2 = len - len1;
    String A = num1.substring(0, len1);
    String B = num1.substring(len1);
    String C = num2.substring(0, len1);
    String D = num2.substring(len1);
    // 乘法法则,分块处理
    int lenM = Math.max(len1, len2);
    String AC = multiply(A, C, len1);
    String AD = multiply(A, D, lenM);
    String BC = multiply(B, C, lenM);
    String BD = multiply(B, D, len2);
    // 注意处理进位的方法,巧妙地运用了字符串的拼接方面
    // 【1】 处理BD,得到原位及进位
    String[] sBD = dealString(BD, len2);
    // 【2】 处理AD + BC的和
    String ADBC = add(AD, BC);
    // 【3】 加上BD的进位
    if (!"0".equals(sBD[1])) {
        ADBC = add(ADBC, sBD[1]);
    }
    // 【4】 得到ADBC的进位
    String[] sADBC = dealString(ADBC, lenM);
    // 【5】 AC加上ADBC的进位
    AC = add(AC, sADBC[1]);
    // 【6】 最终结果
    str = AC + sADBC[0] + sBD[0];
    return str;
}

// 格式化操作的数字字符串,高位补零
private static String formatNumber(String num, int len) {
    while (len > num.length()) {
        num = "0" + num;
    }
    return num;
}

// 两个数字串按位加
private static String add(String ad, String bc) {
    // 返回的结果
    String str = "";
    // 两字符串长度要相同
    int lenM = Math.max(ad.length(), bc.length());
    ad = formatNumber(ad, lenM);
    bc = formatNumber(bc, lenM);
    // 按位加,进位存储在flag中
    int flag = 0;
    // 按序从后往前按位求和
    for (int i = lenM - 1; i >= 0; i--) {
        int t = flag + Integer.parseInt(ad.substring(i, i + 1))
                + Integer.parseInt(bc.substring(i, i + 1));
        // 结果超过9,则进位当前位,保留个位数
        if (t > 9) {
            flag = 1;
            t = t - 10;
        } else {
            flag = 0;
        }
        // 拼接结果字符串
        str = "" + t + str;
    }
    if (flag != 0) {
        str = "" + flag + str;
    }
    return str;
}

// 处理数字串,分离出进位,String数组第一个为原位数字,第二个为进位
private static String[] dealString(String ac, int len) {
    String[] str = {ac, "0"};
    if (len < ac.length()) {
        int t = ac.length() - len;
        str[0] = ac.substring(t);
        str[1] = ac.substring(0, t);
    } else {
        // 保证结果length与len一致,少于则高位补0
        String result = str[0];
        for (int i = result.length(); i < len; i++) {
            result = "0" + result;
        }
        str[0] = result;
    }
    return str;
}

四、其他

1.谈谈自身的优缺点。

发布了90 篇原创文章 · 获赞 10 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/Fan0628/article/details/99974221