Java后端面试题(算法)

 

1、写⼀个字符串反转函数。

方法一:(利用递归实现)

public static String reverse1(String s) {
  int length = s.length();
  if (length <= 1)
   return s;
  String left = s.substring(0, length / 2);
  String right = s.substring(length / 2, length);
  return reverse1(right) + reverse1(left);  //调用递归
 }

方法二:(拼接字符串)

public static String reverse2(String s) {
  int length = s.length();
  String reverse = "";
  for (int i = 0; i < length; i++)
   reverse = s.charAt(i) + reverse;
  return reverse;
 }

方法三:(利用数组,倒序输出)

public static String reverse3(String s) {
  char[] array = s.toCharArray();
  String reverse = "";
  for (int i = array.length - 1; i >= 0; i--)
   reverse += array[i];
  return reverse;
 }

方法四:(利用StringBuffer的内置reverse方法)

public static String reverse4(String s) {
  return new StringBuffer(s).reverse().toString();
 }

方法五:(利用临时变量,交换两头数值)

public static String reverse5(String orig) {
  char[] s = orig.toCharArray();
  int n = s.length - 1;
  int halfLength = n / 2;
  for (int i = 0; i <= halfLength; i++) {
   char temp = s[i];
   s[i] = s[n - i];
   s[n - i] = temp;
  }
  return new String(s);
 }

方法六:(利用位异或操作,交换两头数据)

public static String reverse6(String s) {
  
  char[] str = s.toCharArray();
  
  int begin = 0;
  int end = s.length() - 1;
  while (begin < end) {
   str[begin] = (char) (str[begin] ^ str[end]);
   str[end] = (char) (str[begin] ^ str[end]);
   str[begin] = (char) (str[end] ^ str[begin]);
   begin++;
   end--;
  }
  return new String(str);
 }

方法七:(利用栈结构)

public static String reverse7(String s) {
  char[] str = s.toCharArray();
  Stack<Character> stack = new Stack<Character>();
  for (int i = 0; i < str.length; i++)
   stack.push(str[i]);
  
  String reversed = "";
  for (int i = 0; i < str.length; i++)
   reversed += stack.pop();
  
  return reversed;
 }

2、冒泡排序,冒泡排序的优化⽅案。

原理:比较两个相邻的元素,将值大的元素交换至右端。

思路:设数组的长度为N: 

  1. 比较前后相邻的二个数据,如果前面数据大于后面的数据,就将这二个数据交换。
  2. 这样对数组的第0个数据到N-1个数据进行一次遍历后,最大的一个数据就“沉”到数组第N-1个位置。
  3. N=N-1,如果N不为0就重复前面二步,否则排序完成。
/**
 * 冒泡排序的第一种实现, 没有任何优化
 * @param a
 * @param n
 */
public static void bubbleSort1(int [] a, int n){
    int i, j;

    for(i=0; i<n; i++){//表示n次排序过程。
        for(j=1; j<n-i; j++){
            if(a[j-1] > a[j]){//前面的数字大于后面的数字就交换
                //交换a[j-1]和a[j]
                int temp;
                temp = a[j-1];
                a[j-1] = a[j];
                a[j]=temp;
            }
        }
    }
}

测试代码:

public static void main(String[] args) {
    int[] arr = {1,1,2,0,9,3,12,7,8,3,4,65,22};

    BubbleSort.bubbleSort1(arr, arr.length);

    for(int i:arr){
        System.out.print(i+",");
    }
}

运行结果:

0,1,1,2,3,3,4,7,8,9,12,22,65,

优化一:如果对于一个本身有序的序列,或则序列后面一大部分都是有序的序列,上面的算法就会浪费很多的时间开销,这里设置一个标志flag,如果这一趟发生了交换,则为true,否则为false。明显如果有一趟没有发生交换,说明排序已经完成。

/**
 * 设置一个标志,如果这一趟发生了交换,则为true,否则为false。明显如果有一趟没有发生交换,说明排序已经完成。
 * @param a
 * @param n
 */
public static void bubbleSort2(int [] a, int n){
    int j, k = n;
    boolean flag = true;//发生了交换就为true, 没发生就为false,第一次判断时必须标志位true。
    while (flag){
        flag=false;//每次开始排序前,都设置flag为未排序过
        for(j=1; j<k; j++){
            if(a[j-1] > a[j]){//前面的数字大于后面的数字就交换
                //交换a[j-1]和a[j]
                int temp;
                temp = a[j-1];
                a[j-1] = a[j];
                a[j]=temp;

                //表示交换过数据;
                flag = true;
            }
        }
        k--;//减小一次排序的尾边界
    }
}

运行测试main函数结果:

0,1,1,2,3,3,4,7,8,9,12,22,65,

优化二:比如,现在有一个包含1000个数的数组,仅前面100个无序,后面900个都已排好序且都大于前面100个数字,那么在第一趟遍历后,最后发生交换的位置必定小于100,且这个位置之后的数据必定已经有序了,也就是这个位置以后的数据不需要再排序了,于是记录下这位置,第二次只要从数组头部遍历到这个位置就可以了。如果是对于上面的冒泡排序算法2来说,虽然也只排序100次,但是前面的100次排序每次都要对后面的900个数据进行比较,而对于现在的排序算法3,只需要有一次比较后面的900个数据,之后就会设置尾边界,保证后面的900个数据不再被排序。

public static void bubbleSort3(int [] a, int n){
    int j , k;
    int flag = n ;//flag来记录最后交换的位置,也就是排序的尾边界

    while (flag > 0){//排序未结束标志
        k = flag; //k 来记录遍历的尾边界
        flag = 0;

        for(j=1; j<k; j++){
            if(a[j-1] > a[j]){//前面的数字大于后面的数字就交换
                //交换a[j-1]和a[j]
                int temp;
                temp = a[j-1];
                a[j-1] = a[j];
                a[j]=temp;
                //表示交换过数据;
                flag = j;//记录最新的尾边界.
            }
        }
    }
}

运行测试例子结果:

0,1,1,2,3,3,4,7,8,9,12,22,65,

时间复杂度:

      若记录序列的初始状态为"正序",则冒泡排序过程只需进行一趟排序,在排序过程中只需进行n-1次比较,且不移动记录;反之,若记录序列的初始状态为"逆序",则需进行n(n-1)/2次比较和记录移动。因此冒泡排序总的时间复杂度为O(n*n)。

3.快速排序。 快排的最优时间复杂度,最差复杂度。

        快排的基本思想: 通过选择的参考值将待排序记录分割成独立的两部分,一部分全小于选取的参考值,另一部分全大于选取的参考值。对分割之后的部分再进行同样的操作直到无法再进行该操作位置(可以使用递归)。

选择数组的第一个元素为参考值。

import java.util.Arrays;

public class QuickSort {

    public static void main(String[] args) {
        int[] num = { 1, 3, 4, 8, 5, 10, 22, 15, 16 };
        QuickSort.quickSort(num, 0, num.length - 1);
        System.out.println(Arrays.toString(num));
    }

    public static void quickSort(int[] a, int start, int end) {
        // 该值定义了从哪个位置开始分割数组
        int ref;
        if (start < end) {
            // 调用partition方法对数组进行排序
            ref = partition(a, start, end);
            // 对分割之后的两个数组继续进行排序
            quickSort(a, start, ref - 1);
            quickSort(a, ref + 1, end);
        }
    }

    /**
     * 选定参考值对给定数组进行一趟快速排序
     * 
     * @param a  数组
     * @param start (切分)每个数组的第一个的元素的位置
     * @param end (切分)每个数组的最后一个的元素位置
     * @return 下一次要切割数组的位置
     */

    public static int partition(int[] a, int start, int end) {
        // 取数组的第一个值作为参考值(关键数据)
        int refvalue = a[start];
        // 从数组的右边开始往左遍历,直到找到小于参考值的元素
        while (start < end) {
            while (end > start && a[end] >= refvalue) {
                end--;
            }
            // 将元素直接赋予给左边第一个元素,即pivotkey所在的位置
            a[start] = a[end];
            // 从序列的左边边开始往右遍历,直到找到大于基准值的元素
            while (end > start && a[start] <= refvalue) {
                start++;
            }
            a[end] = a[start];
            return end;
        }
        // 最后的start是基准值所在的位置
        a[start] = refvalue;
        return start;
    }
}

时间复杂度分析:

  • 在最优的情况下,Partition每次都划分得很均匀,快速排序算法的时间复杂度为O(nlogn)。
  • 最糟糕情况下的快排,当待排序的序列为正序或逆序排列时,时间复杂度为O(n^2)。

空间复杂度分析:

  • 最好情况,递归树的深度为log2n,其空间复杂度也就为O(logn)
  • 最坏情况,需要进行n‐1递归调用,其空间复杂度为O(n),平均情况,空间复杂度也为O(logn)。

一种简单优化的方式:

三向切分快速排序 :核心思想就是将待排序的数据分为三部分,左边都小于比较值,右边都大于比较值,中间的数和比较值相等.三向切分快速排序的特性就是遇到和比较值相同时,不进行数据交换, 这样对于有大量重复数据的排序时,三向切分快速排序算法就会优于普通快速排序算法,但由于它整体判断代码比普通快速排序多一点,所以对于常见的大量非重复数据,它并不能比普通快速排序多大多的优势 。

4、归并排序

两路归并排序算法思路

分而治之(divide - conquer);每个递归过程涉及三个步骤
第一, 分解: 把待排序的 n 个元素的序列分解成两个子序列, 每个子序列包括 n/2 个元素.
第二, 治理: 对每个子序列分别调用归并排序MergeSort, 进行递归操作
第三, 合并: 合并两个排好序的子序列,生成排序结果.

算法实现

此算法的实现不像图示那样简单,现分三步来讨论。首先从宏观上分析,首先让子表表长 L=1 进行处理;不断地使 L=2*L ,进行子表处理,直到 L>=n 为止,把这一过程写成一个主体框架函数 mergesort 。然后对于某确定的子表表长 L ,将 n 个记录分成若干组子表,两两归并,这里显然要循环若干次,把这一步写成一个函数 mergepass ,可由 mergesort 调用。最后再看每一组(一对)子表的归并,其原理是相同的,只是子表表长不同,换句话说,是子表的首记录号与尾记录号不同,把这个归并操作作为核心算法写成函数 merge ,由 mergepass 来调用。假设我们有一个没有排好序的序列,那么首先我们使用分割的办法将这个序列分割成一个一个已经排好序的子序列,然后再利用归并的方法将一个个的子序列合并成排序好的序列。分割和归并的过程可以看下面的图例。

代码实现:

public static int[] sort(int[] a,int low,int high){
        int mid = (low+high)/2;
        if(low<high){
            sort(a,low,mid);
            sort(a,mid+1,high);
            //左右归并
            merge(a,low,mid,high);
        }
        return a;
    }
     
    public static void merge(int[] a, int low, int mid, int high) {
        int[] temp = new int[high-low+1];
        int i= low;
        int j = mid+1;
        int k=0;
        // 把较小的数先移到新数组中
        while(i<=mid && j<=high){
            if(a[i]<a[j]){
                temp[k++] = a[i++];
            }else{
                temp[k++] = a[j++];
            }
        }
        // 把左边剩余的数移入数组 
        while(i<=mid){
            temp[k++] = a[i++];
        }
        // 把右边边剩余的数移入数组
        while(j<=high){
            temp[k++] = a[j++];
        }
        // 把新数组中的数覆盖nums数组
        for(int x=0;x<temp.length;x++){
            a[x+low] = temp[x];
        }
    }

算法分析:

(1)稳定性
      归并排序是一种稳定的排序。
(2)存储结构要求
     可用顺序存储结构。也易于在链表上实现。
(3)时间复杂度
     对长度为n的文件,需进行趟二路归并,每趟归并的时间为O(n),故其时间复杂度无论是在最好情况下还是在最坏情况下均是O(nlgn)。
(4)空间复杂度
     需要一个辅助向量来暂存两有序子文件归并的结果,故其辅助空间复杂度为O(n),显然它不是就地排序。
  注意:
     若用单链表做存储结构,很容易给出就地的归并排序

5、单向链表,查找中间的那个元素。

设置两个指针,一个快指针,每次走两步,一个慢指针,每次走一步。

public class searchMid {
	public  Node method(Node head) {
		Node p=head;
		Node q=head;
		while(q!=null&&q.next!=null&&q.next.next!=null) {
			p=p.next;
			q=q.next.next;
		}
		return p;
	} 
}

6、⼆分查找的时间复杂度,优势。

二分查找又称折半查找,它是一种效率较高的查找方法。

折半查找的算法思想是将数列按有序化(递增或递减)排列,查找过程中采用跳跃式方式查找,即先以有序数列的中点位置为比较对象,如果要找的元素值小 于该中点元素,则将待查序列缩小为左半部分,否则为右半部分。通过一次比较,将查找区间缩小一半。 折半查找是一种高效的查找方法。它可以明显减少比较次数,提高查找效率。但是,折半查找的先决条件是查找表中的数据元素必须有序。

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

二分算法步骤描述

① 首先确定整个查找区间的中间位置 mid = ( left + right )/ 2

② 用待查关键字值与中间位置的关键字值进行比较;

若相等,则查找成功

若大于,则在后(右)半个区域继续进行折半查找

若小于,则在前(左)半个区域继续进行折半查找

③ 对确定的缩小区域再按折半公式,重复上述步骤。

最后,得到结果:要么查找成功, 要么查找失败。折半查找的存储结构采用一维数组存放。 折半查找算法举例

对给定数列(有序){ 3,5,11,17,21,23,28,30,32,50,64,78,81,95,101},按折半查找算法,查找关键字值为81的数据元素。

二分查找算法讨论:

优点:ASL≤log2n,即每经过一次比较,查找范围就缩小一半。经log2n 次计较就可以完成查找过程。

缺点:因要求有序,所以要求查找数列必须有序,而对所有数据元素按大小排序是非常费时的操作。另外,顺序存储结构的插入、删除操作不便利。

考虑:能否通过一次比较抛弃更多的部分(即经过一次比较,使查找范围缩得更小),以达到提高效率的目的。……?

可以考虑把两种方法(顺序查找和折半查找)结合起来,即取顺序查找简单和折半查找高效之所长,来达到提高效率的目的?实际上这就是分块查找的算法思想。

public class BinarySearch { 
        /** 
        * 二分查找算法 
        * 
        * @param srcArray 有序数组 
        * @param key 查找元素 
        * @return key的数组下标,没找到返回-1 
        */  
        public static void main(String[] args) { 
            int srcArray[] = {3,5,11,17,21,23,28,30,32,50,64,78,81,95,101};   
            System.out.println(binSearch(srcArray, 0, srcArray.length - 1, 81));  
        } 

        // 二分查找递归实现   
        public static int binSearch(int srcArray[], int start, int end, int key) {   
            int mid = (end - start) / 2 + start;   
            if (srcArray[mid] == key) {   
                return mid;   
            }   
            if (start >= end) {   
                return -1;   
            } else if (key > srcArray[mid]) {   
                return binSearch(srcArray, mid + 1, end, key);   
            } else if (key < srcArray[mid]) {   
                return binSearch(srcArray, start, mid - 1, key);   
            }   
            return -1;   
        } 

        // 二分查找普通循环实现   
        public static int binSearch(int srcArray[], int key) {   
            int mid = srcArray.length / 2;   
            if (key == srcArray[mid]) {   
                return mid;   
            }   

            int start = 0;   
            int end = srcArray.length - 1;   
            while (start <= end) {   
                mid = (end - start) / 2 + start;   
                if (key < srcArray[mid]) {   
                   end = mid - 1;   
                } else if (key > srcArray[mid]) {   
                    start = mid + 1;   
                } else {   
                    return mid;   
                }   
            }   
            return -1;   
        } 
    }

7、⼀个单向链表,删除倒数第N个数据。

public ListNode removeNthFromEnd(ListNode head, int n) 
	{
		//求出链表长度
		int len = 0;//链表长度
		ListNode p,q;
		p = head;
		while(p!=null)
		{
			len++;
			p = p.next;
		}
		if(n>len)
			return null;
		
		p = head;
		q = head;
		for(int i = 0;i<len-n;i++)
		{
			p = q;
			q = q.next;
		}
		//当删除的是第一个节点的时候
		if(p==q)
			head = p.next;
		if(q!=null)
		{
			p.next = q.next;
			q.next = null;
		}
			
		return head;
    }

8、遍历二叉树

public class BinaryTree {
 
 int data;      //根节点数据
 BinaryTree left;    //左子树
 BinaryTree right;   //右子树
 
 public BinaryTree(int data)    //实例化二叉树类
 {
  this.data = data;
  left = null;
  right = null;
 }
 
 public void insert(BinaryTree root,int data){     //向二叉树中插入子节点
  if(data>root.data)                               //二叉树的左节点都比根节点小
  {
   if(root.right==null){
    root.right = new BinaryTree(data);
   }else{
    this.insert(root.right, data);
   }
  }else{                                          //二叉树的右节点都比根节点大
   if(root.left==null){
    root.left = new BinaryTree(data);
   }else{
    this.insert(root.left, data);
   }
  }
 }
}

当建立好二叉树类后可以创建二叉树实例,并实现二叉树的先根遍历,中根遍历,后根遍历,代码如下:

public class BinaryTreePreorder {
 
 public static void preOrder(BinaryTree root){  //先根遍历
  if(root!=null){
   System.out.print(root.data+"-");
   preOrder(root.left);
   preOrder(root.right);
  }
 }
 
 public static void inOrder(BinaryTree root){     //中根遍历

  if(root!=null){
   inOrder(root.left);
   System.out.print(root.data+"--");
   inOrder(root.right);
  }
 }
 
 public static void postOrder(BinaryTree root){    //后根遍历

  if(root!=null){
   postOrder(root.left);
   postOrder(root.right);
   System.out.print(root.data+"---");
  }
 }
 
 public static void main(String[] str){
  int[] array = {12,76,35,22,16,48,90,46,9,40};
  BinaryTree root = new BinaryTree(array[0]);   //创建二叉树
  for(int i=1;i<array.length;i++){
   root.insert(root, array[i]);       //向二叉树中插入数据
  }
  System.out.println("先根遍历:");
  preOrder(root);
  System.out.println();
  System.out.println("中根遍历:");
  inOrder(root);
  System.out.println();
  System.out.println("后根遍历:");
  postOrder(root);
}

  创建好的二叉树图形如下:

当运行上面的程序后结果如下:

先根遍历:
12-9-76-35-22-16-48-46-40-90-
中根遍历:
9--12--16--22--35--40--46--48--76--90--
后根遍历:
9---16---22---40---46---48---35---90---76---12---

9、在10亿个数字里找最小的10个。

10、有1亿个数字,其中有2个是重复的,快速找到它,时间和空间要最优。

11、2亿个随机生成的无序整数,找出中间大小的值。

12、给⼀个不知道长度的(可能很⼤)输⼊字符串,设计⼀种⽅案,将重复的字符排重。

13、有3n+1个数字,其中3n个中是重复的,只有1个是不重复的,怎么找出来。

14、⼀个已经构建好的TreeSet,怎么完成倒排序。

15、什么是B+树,B-树,列出实际的使用场景。

16、200个有序的数组,每个数组⾥⾯100个元素,找出top20的元素。

猜你喜欢

转载自blog.csdn.net/qq_21822741/article/details/84580735