大家好,很高兴我们可以继续学习交流Java相关面试题目。本小节开始,我们主要进行高频算法题目的讲解。“手撕算法”应该算是技术岗位最通用的面试题目了。

在各大公司的面试中,有一个最基本的要求,那就是必须写点代码。技术面试一般情况下可以归纳为三大块,即业务逻辑面试,基础技术面试和算法面试。

第8章 第1节 高频面试算法 - 基础(上)

业务逻辑面试就是让你讲述你的项目,并且进行针对性提问,考察你对项目是否足够熟悉与了解。基础技术面试就比较广了,所有涉及到的相关技术知识点都可以考察。一般情况下,面试官会留出20分钟左右的时间和我们一起研究探讨算法。

对于服务端开发同学来说,算法面试及其重要。在校招的面试中,一个算法题是否有思路并且可以完整的写出来,很多时候都直接决定了这轮面试的结果,因为校招毕竟是相当注重基础的考察。在社招的面试上,本轮的面试结果也会很大程度上受到算法题表现的影响。

为什么算法面试的重要性这么高?

  • 首先,算法是一种通用的考察点,任何技术岗面试都可以进行考察。
  • 其次,算法包含了太多的逻辑思维,可以考察应聘者思考问题的逻辑和解决问题的能力。
  • 最后,连这么有难度的算法题你都可以搞定,那么其他只需要看看写写用用就可以掌握的基础知识和相关技术框架还怕学不会吗?

 

应该去哪里练习算法题呢?

  • 当然是去牛客网啦,牛客网是一个专注于程序员的学习和成长的专业平台,集笔试面试系统、课程教育、社群交流、招聘内推于一体。我们可以在在线编程模块进行算法题的练习,对于找工作是一个不可多得的好帮手。
  • 然后是LeetCode,这是一个美国的在线编程网站,上面主要收集了各大IT公司的笔试面试题, LeetCode的主要特点就是按照难易将题目分为了easy、medium和hard三种,并且在LeetCode上将题目进行了分类。在自己练习通过之后可以打开讨论区,看看别人解决问题的思路。

 

在本次的专刊中,我们主要从下边的七个考察点来交流,精选其中最高频的算法题目与大家进行交流。

  • 排序和查找算法
  • 单链表
  • 二叉树
  • 队列和栈
  • 字符串
  • 数组
  • 其它算法

 

本小节中,我们主要交流探讨排序与查找算法,常见链表和二叉树的考察算法。好了,让我们一起来交流吧。
 

(1)排序与查找算法:

关于排序和查找算法的考察,在校招面试和社招面试中都是极其热门的,不外乎别的,仅仅是因为这就是最基础并且常用的算法。

排序算法分类:

常见的排序算法从排序方式上可以分为四种,即选择排序,交换排序,插入排序以及归并排序。

  • 选择排序:直接选择排序和堆排序
  • 交换排序:冒泡排序和快速排序
  • 插入排序:直接插入排序,二分插入排序和希尔排序
  • 归并排序:归并排序

 

关于排序算法的考察,考察点包括每一个排序算法的原理(排序方式),时间空间复杂度以及判断其是否稳定(得会分析)。这里我们给出最常见的快速排序的算法实现。
 

快速排序算法:

快速排序是一种分区交换排序算法。采用分治策略对两个子序列再分别进行快速排序,是一种递归算法。

算法描述:

在数据序列中选择一个元素作为基准值,每趟从数据序列的两端开始交替进行,将小于基准值的元素交换到序列前端,将大于基准值的元素交换到序列后端,介于两者之间的位置则成为了基准值的最终位置。同时,序列被划分成两个子序列,再分别对两个子序列进行快速排序,直到子序列的长度为1,则完成排序。

举例:

假设要排序的数组是a[6],长度为7,首先任意选取一个数据(通常选用第一个数据)作为关键数据,然后将所有比它小的数都放到它前面,所有比它大的数都放到它后面,这个过程称为一躺快速排序。一躺快速排序的算法是:

  • 设置两个变量i,j,排序开始的时候i=0;j=6;
  • 以第一个数组元素作为关键数据,赋值给key,即key=a[0];
  • 从j开始向前搜索,即由后开始向前搜索(j--),找到第一个小于key的值,两者交换;
  • 从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的值,两者交换;
  • 重复第3、4步,直到i=j;此时将key赋值给a[i];

 

例如:待排序的数组a的值分别是:(初始关键数据key=49)

第8章 第1节 高频面试算法 - 基础(上)

此时完成了一趟循环,将49赋值给a[3],数据分为三组,分别为{27,38,13}{49}{76,96,65},利用递归,分别对第一组和第三组进行排序,则可得到一个有序序列,这就是快速排序算法。

算法实现:

复制代码

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

public void quickSort(int[] num, int left, int right) {

    if (num == null)

        return//如果左边大于右边,则return,这里是递归的终点,需要写在前面。

    if (left >= right) {

        return;

    }

    int i = left;

    int j = right;

    int temp = num[i]; //此处开始进入遍历循环

    while (i < j) { //从右往左循环

        while (i < j && num[j] >= temp) {//如果num[j]大于temp值,则pass,比较下一个

            j--;

        }

        num[i] = num[j];

        while (i < j && num[i] <= temp) {

            i++;

        }

        num[j] = num[i];

        num[i] = temp; // 此处不可遗漏,将基准值插入到指定位置

    }

    quickSort(num, left, i - 1);

    quickSort(num, i + 1, right);

}

 

查找算法:

关于查找算法的考察主要是二分查找算法。二分查找又称折半查找,优点是比较次数少,查找速度快,平均性能好;其缺点是要求待查表为有序表,且插入删除困难。因此,折半查找方法适用于不经常变动而查找频繁的有序列表。

二分查找算法实现步骤:

  • 首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功。
  • 否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前面子表,否则进一步查找后面子表。
  • 重复以上过程,直到找到满足条件的记录,使查找成功,或一直到子表不存在为止,此时查找失败。

 

算法前提:必须采用顺序存储结构;必须按关键字大小有序排列。

实现方式:包含递归实现和非递归实现两种方式。

递归实现方式如下:

复制代码

1

2

3

4

5

6

7

8

9

10

11

12

13

14

private  int halfSearch(int[] a,int left,int right,int target) {

    int mid=left+(right-left)/2// 防止整型溢出

    int midValue=a[mid];

    if(left<=right){

        if(midValue>target){

            return halfSearch(a, left, mid-1, target);

        }else if(midValue<target) {

            return halfSearch(a, mid+1, right, target);

        }else {

            return mid;

        }

    }

    return -1;

}

 

非递归实现方式如下:

复制代码

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

private  int halfSearch(int[] a,int target){

    int i=0;

    int j=a.length-1;

    while(i<=j){

        int mid=i+(j-i)/2;

        int midValue=a[mid];

        if(midValue>target){

            j=mid-1;

        }else if(midValue<target){

            i=mid+1;

        }else {

            return mid;

        }

    }

    return -1;

}

 

排序与查找算法总结:

关于排序与查找算法,必须熟练完成常见算法的手写。包括快速排序,冒泡排序以及选择排序等。另外,排序算法的时间和空间复杂度也需要掌握。不要觉得这些都很简单,那么我来提个问题吧~

选择排序和冒泡排序的区别是什么?

小伙伴们可以在文章下边评论,我们一起交流学习~

 

(2)链表相关算法考察:

链表是一种在物理存储单元上非连续,非顺序的存储结构,由一系列结点组成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域

第8章 第1节 高频面试算法 - 基础(上)

链表是一种常见的数据结构,由于对链表的操作往往涉及到了指针的移动,所以也是面试中常见的算法考察知识点。链表分为了单向链表,双向链表和循环链表。在面试中,由于时间有限,一般情况下都在考察单向链表。

我们可以定义如下:

复制代码

1

2

3

4

5

6

7

class Node{ 

    int val; 

    Node next; 

    public Node(int val){ 

         this.val=val; 

    }

}

 

链表考察点:链表的操作主要就是对指针的操作,常见面试题目都在考察指针的操作是否合理与正确。
 

链表常见算法题:

  • 单链表反转
  • 合并有序单链表
  • 找出单链表的中间节点
  • 判断单链表相交或者有环
  • 找出进入环的第一个节点
  • 求单链表相交的第一个节点

 

典型例题分析:

鉴于面试时间有限,一道算法题目的时间通常不会超出20分钟,所以都会挑着最高频的题目进行考察,那么我们就来一起看两道常见链表算法题目吧。
 

单链表反转: 比如1→2→3→4→5,反转之后返回5→4→3→2→1

反转步骤:

  • 从头到尾遍历原链表,每遍历一个结点
  • 将其摘下放在新链表的最前端。
  • 注意链表为空和只有一个结点的情况。

 

算法实现:

复制代码

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

public static Node reverseNode(Node head){ 

      // 如果链表为空或只有一个节点,无需反转,直接返回原链表表头 

      if(head == null || head.next == null

          return head; 

 

      Node reHead = null

      Node cur = head; 

      while(cur!=null){ 

          Node reCur = cur;      // 用reCur保存住对要处理节点的引用 

          cur = cur.next;        // cur更新到下一个节点 

          reCur.next = reHead;   // 更新要处理节点的next引用 

          reHead = reCur;        // reHead指向要处理节点的前一个节点 

      

      return reHead; 

 }

 

合并两个有序的单链表:给出两个分别有序的单链表,将其合并成一条新的有序单链表。

举例:1→3→5和2→4→6合并之后为1→2→3→4→5→6

步骤:

  • 首先,我们通过比较确定新链表的头节点,然后移动链表1或者链表2的头指针。
  • 然后通过递归来得到新的链表头结点的next

 

算法实现:

复制代码

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

public static Node mergeList(Node list1 , Node list2){ 

    if(list1==null

        return list2; 

    if(list2==null

        return list1; 

    Node resultNode; 

    if(list1.val<list2.val){ // 通过比较大小,得到新的节点

        resultNode = list1; 

        list1 = list1.next; 

    }else

        resultNode = list2; 

        list2 = list2.next; 

    

    // 递归得到next

    resultNode.next = mergeList(list1, list2); 

    return resultNode; 

}

 

链表总结:

关于链表这块的算法题目,我们一般采用多个指针即可搞定。由于面试时间有限,我们重点掌握单链表的反转,更进一步,面试中会考察单链表的分段反转。单链表的多段反转核心其实还是单链表的反转,这个算法留给小伙伴自行学习。

 

(3)二叉树相关算法考察:

二叉树是每个结点最多有两个子树的树结构。关于树的相关术语和性质,我们这里就不详细介绍了,请自行查阅相关资料。我们来解释下树的遍历方式吧。树的遍历方式分为深度优先方式和广度优先方式。假设,现在有这么一颗二叉树,我们来看看其遍历方式。

第8章 第1节 高频面试算法 - 基础(上)
 

深度优先遍历:

深度优先遍历方式分为了前序遍历,中序遍历和后序遍历。前中后指的是根root的遍历顺序。

  • 前序遍历:根节点-左子树-右子树的遍历方式,a-b-d-f-e-g-c
  • 中序遍历:左子树-根节点-右子树的遍历方式,d-f-b-g-e-a-c
  • 后序遍历:左子树-右子树-根节点的遍历方式,f-d-g-e-b-c-a

 

宽度优先遍历:

宽度优先遍历其实就是分层遍历方式,也就是按照二叉树的每一层依次遍历输出。对应我们的示例树,那就是a-b-c-d-e-f-g。
 

二叉树的考察点:

二叉树的考察,主要是对指针的考察。出现最多的就是深度优先和宽度优先遍历方式的考察。在各个遍历方式中,不光涉及到了指针的移动,还涉及到了辅助数据结构,比如说队列和堆栈的使用。
 

二叉树常见算法题:

  • 分层遍历(宽度优先遍历)
  • 前序遍历,中序遍历,后序遍历
  • 求二叉树中的节点个数
  • 求二叉树的深度
  • 求二叉树第K层的节点个数
  • 求二叉树中叶子节点的个数
  • 判断两颗二叉树是否是相同的树
  • 求二叉树中两个节点的最低公共祖先节点

 

典型例题分析:

同样的,限于面试时间有限,我们这里挑选最高频的二叉树面试题目进行分析。在例题开始之前,我们先来定义如下的二叉树结构:

复制代码

1

2

3

4

5

6

7

8

class TreeNode {

    int val;

    TreeNode left;

    TreeNode right;

    public TreeNode(int val) {

        this.val = val;

    }

}

 

二叉树的分层遍历:

给出如下的二叉树,输出其分层遍历结果a-b-c-d-e-f-g。

第8章 第1节 高频面试算法 - 基础(上)
 

步骤:

分层遍历符合先进先出的规律,可以使用队列做为辅助数据结构。

  • 队列初始化,将根节点压入队列。
  • 当队列不为空,进行如下操作:弹出一个节点,访问,若左子节点或右子节点不为空,将其压入队列 。
    第8章 第1节 高频面试算法 - 基础(上)

 

算法实现:

复制代码

1

2

3

4

5

6

7

8

9

10

11

12

13

14

public static void levelTraversal(TreeNode root){ 

    if(root==null

        return

    LinkedList<TreeNode> queue = new LinkedList<TreeNode>(); 

    queue.add(root);  // 队列初始化,将根节点加入队列

    while(!queue.isEmpty()){ 

        TreeNode cur = queue.remove(); 

        System.out.print(cur.val+" "); 

        if(cur.left!=null

            queue.add(cur.left); 

        if(cur.right!=null

            queue.add(cur.right); 

    

}

 

二叉树的前序遍历:

还是同样的一颗二叉树,要求输出其前序遍历a-b-d-f-e-g-c。

第8章 第1节 高频面试算法 - 基础(上)
 

分析:

在二叉树的前序遍历中,遍历顺序为根左右,也就是左节点在右节点之前,可以考虑使用堆栈这种后进先出的数据结构来实现。
 

步骤:

  • 使用一个辅助堆栈,初始时将根节点加入堆栈。
  • 当堆栈不为空时,弹出一个节点,分别将其不为空的右子节点和左子节点加入堆栈。

第8章 第1节 高频面试算法 - 基础(上)
 

算法实现:

复制代码

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

public static void preorderTraversal(TreeNode root){ 

    if(root==null

        return

    Stack<TreeNode> stack = new Stack<TreeNode>(); // 辅助stack 

    stack.push(root); 

    while(!stack.isEmpty()){ 

        TreeNode cur = stack.pop();  // 出栈栈顶元素  

        System.out.print(cur.val+" "); 

        // 关键点:要先压入右孩子,再压入左孩子,这样在出栈时会先打印左孩子再打印右孩子 

        if(cur.right!=null

             stack.push(cur.right); 

        if(cur.left!=null

             stack.push(cur.left); 

    

}

 

二叉树总结:

关于二叉树的面试题准备中,我们一定要装备好常见的分层遍历和前中后序遍历的实现方式,这些遍历中使用的辅助数据结构为先进先出的队列以及后进先出的堆栈。宽度优先和深度优先遍历做为二叉树考察中最高频的算法题目,希望大家一定加以理解与掌握。说白了,如果面试中考察你遍历方式,那就应该是送分题目。

 

本节总结:

在本小节中,我们较为详细的给大家阐述了排序与查找算法,链表以及二叉树的相关高频面试题目。由于,我们并不是在进行算法与数据结构的入门,所以挑选了面试中最常出现的高频题目来进行了讲解与分析。这些题目,说白了就是送分题目,希望大家可以有效理解与掌握,把握住机会。

相关文章:

  • 2021-12-02
  • 2021-06-21
  • 2021-07-03
  • 2021-07-23
  • 2022-12-23
  • 2022-12-23
猜你喜欢
  • 2021-06-20
  • 2021-06-14
  • 2021-11-21
  • 2021-08-22
  • 2022-12-23
  • 2022-12-23
  • 2022-01-22
相关资源
相似解决方案