面试总结之算法题

在一个多月的面试过程中,有很多需要讲思路或实现的算法或其他题目,这里总结下作为复习。后面不断更新。

算法题

1. 翻转链表

题目描述:

给你一个链表,使用方法返回一个翻转的链表,如1->2->3->4->5,返回的结果为5->4->3->2->1。

思路

自己的思路:
先使用一个list存储链表的所有节点值,再倒序遍历,拼接一个链表即可。

/**
     * 先按自己想法实现一遍
     * 存到数组再从后往前遍历
     * @param head
     * @return
     */
    public ListNode ReverseList(ListNode head) {

        ArrayList<Integer> list = new ArrayList();

        while (head != null) {
            list.add(head.val);
            head = head.next;
        }

        System.out.println("list: " + list);

        int len = list.size();
        ListNode node = new ListNode(list.get(len - 1));
        for (int i = len - 2; i >= 0; i --) {
            // 再循环遍历出node的最后一个节点,并拼接进去

            ListNode tmp = node;
            while (true) {
                if (tmp.next == null) {
                    tmp.next = new ListNode(list.get(i));
                    break;
                }
                tmp = tmp.next;

            }
        }


        return node;
    }

其他思路:
如何让后一个节点指向前一个节点!在下面的代码中定义了一个 next 节点,该节点主要是保存要反转到头的那个节点,防止链表 “断裂”。

 // 反转链表
    public ListNode ReverseList(ListNode head) {

        ListNode next = null;
        ListNode pre = null;

        while (head != null) {
            // 保存要反转到头的那个节点
            next = head.next;
            // 要反转的那个节点指向已经反转的上一个节点(备注:第一次反转的时候会指向null)
            head.next = pre;
            // 上一个已经反转到头部的节点
            pre = head;
            // 一直向链表尾走
            head = next;
        }
        return pre;
    }

测试:

 public static void main(String[] args) {

        ListNode a = new ListNode(1);
        ListNode b = new ListNode(2);
        ListNode c = new ListNode(3);
        ListNode d = new ListNode(4);
        ListNode e = new ListNode(5);
        a.next = b;
        b.next = c;
        c.next = d;
        d.next = e;

        ListNode pre = new Demo().ReverseList(a);
//
//        while (e != null) {
//            System.out.println(e.val);
//            e = e.next;
//        }

		// ListNode{val=5, next=ListNode{val=4, next=ListNode{val=3, next=ListNode{val=2, next=ListNode{val=1, next=null}}}}}
        System.out.println(pre);
    }

2. 解析字符串中数学表达式

题目描述:

给你String,类似于“1 + 2 * (3 - 4 * 2) / 4” 这样的字符串,你怎么输出最后结果,说说你的思路。

思路

先将这个后缀表达式利用将其转化为后缀表达式(不包含括号):

从左到右遍历中缀表达式,遇到操作数,输出,遇到操作符,当前操作符的优先级大于栈顶操作符优先级,进栈,否则,弹出栈顶优先级大于等于当前操作符的操作符,当前操作符进栈。

这里还说下后缀表达式的好处,其中不包含括号,运算符是放在两个操作数之后,不用考虑运算符的优先级了。

接下来从左到右遍历后缀表达式,如果遇到操作数,则将其压入栈,遇到运算符,则将栈顶两个元素计算完后压入栈即可,直到遍历完后,栈中的最后数据即为结果。

3. 阻塞队列实现

题目描述:

利用Java编写一个阻塞队列,你的思路是怎么样的?

思路

生产者消费者模型就是当队列为空时消费者停止消费,当队列满时生产者停止生产。利用wait 和 notify 协调生产者线程和消费者线程的关系,再加上一个数组作为队列的容器,生产者的偏移量以及消费者的偏移量就可以完成一个简单的消费者生产者模型。

阻塞队列如下,使用的是ArrayList作为容器:

package top.hellolsy.offer;


import java.util.ArrayList;

/**
 * 自己动手实现一个有界阻塞队列
 * 可以使用synchronize及wait notify
 * 也可以使用Lock的condition
 *
 * 这里使用的是Arraylist作为阻塞队列
 *
 *
 * 生产者消费者模型就是当队列为空时消费者停止消费,当队列满时生产者停止生产。利用wait 和 notify 协调生产者线程和消费者线程的关系,
 * 再加上一个数组作为队列的容器,生产者的偏移量以及消费者的偏移量就可以完成一个简单的消费者生产者模型。
 * 阻塞队列原理就是如此
 *
 */
public class MyArrayBlockQuene {

    /**
     * 队列容器
     */
    private ArrayList<Integer> container = new ArrayList<>();

    /**
     * 元素个数
     * 且使用volatile保证可见性
     */
    private volatile int size;

    /**
     * 容量
     * 且使用volatile保证可见性
     */
    private volatile int capacity;

    /**
     * 锁的对象
     */
    private Object lock = new Object();

    /**
     * 传入容量
     * @param capacity
     */
    public MyArrayBlockQuene(int capacity) {
        this.capacity = capacity;
    }



    /**
     * 生产者生产
     * @param data
     */
    public void put(int data) {

        synchronized (lock) {
            // 队列满了
            while (size == capacity) {

                try {
                    System.out.println("队列已经满了,需要等消费者消费...");
                    // 这里等待同时会释放锁
                    // wait() 方法调用后会使得当前所在的线程暂停运行
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }


            }

            // 队列没满
            System.out.println("生产了:" + data);
            container.add(data);
            size ++;
            // 通知可以取数据
            lock.notifyAll();

        }


    }


    /**
     * 消费者消费
     * @return 返回消费的数据
     */
    public int take() {
        int result = 0;
        synchronized (lock) {
            // 队列空了
            while (size == 0) {

                try {
                    System.out.println("队列已经空了,需要等生产者生产...");
                    // 这里等待同时会释放锁
                    // wait() 方法调用后会使得当前所在的线程暂停运行
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }


            }

            // 队列没满 序号是从0开始,所以需要size - 1
            result = container.remove(size - 1);
            size --;
            // 通知可以生产数据
            lock.notifyAll();

        }



        return result;
    }






}

测试类:

package top.hellolsy.offer;


public class MyBlockQueueTest {
    public static void main(String[] args) {
        // 队列容量为5
        MyArrayBlockQuene queue = new MyArrayBlockQuene(5);

        Thread put_thread = new Thread(() -> {
            for (int i = 0; i < 40; i ++) {
                queue.put(i + 1);
                try {
                    Thread.sleep(500);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        });

        Thread take_thread = new Thread(() -> {
            for(;;){
                System.out.println("消费者开始工作,消费:" + queue.take());
                try {
                    Thread.sleep(800);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        });

        put_thread.start();
        take_thread.start();
    }
}

运行结果:
面试总结之算法题_第1张图片
面试总结之算法题_第2张图片

另外,还有可以使用lock中的Condition实现,由于很少使用lock,我按照我熟悉的synchronized 的等待,通知机制实现的。

4. 排序查找问题

这里包含了常见的选择冒泡插入快速排序二分查找

package top.hellolsy.offer.others;


import java.util.Arrays;

/**
 * 手写排序还有查找的几个常见方法
 *
 * 稳定排序:冒泡 插入 归并 基数
 * 不稳定:选择 快速 希尔 堆
 */
public class Sort {


    public static void main(String[] args) {
        int[] arr = new int[]{6,4,2,8,6,0,9,1};
        int[] arr1 = new int[]{12,45,23,67,7,1,5,21};
        int[] arr2 = new int[]{12,45,23,7,7,1,5,21};
        int[] arr3 = new int[]{12,45,1,67,67,23,5,21};

        System.out.println("未排序之前arr : " + Arrays.toString(arr));
        System.out.println("未排序之前arr1 : " + Arrays.toString(arr1));
        System.out.println("未排序之前arr2 : " + Arrays.toString(arr2));
        System.out.println("未排序之前arr3 : " + Arrays.toString(arr3));
//        select_sort(arr);
//        bubble_sort(arr);
//        insert_sort(arr);
        quick_sort(arr,0, arr.length - 1);
        quick_sort(arr1,0, arr1.length - 1);
        quick_sort(arr2,0, arr2.length - 1);
        quick_sort(arr3,0, arr3.length - 1);

        System.out.println("排序之后arr : " + Arrays.toString(arr));
        System.out.println("排序之后arr1 : " + Arrays.toString(arr1));
        System.out.println("排序之后arr2 : " + Arrays.toString(arr2));
        System.out.println("排序之后arr3 : " + Arrays.toString(arr3));


        int result = binary_search(arr3, 67);

        System.out.println(result);
    }






    /**
     * 选择排序
     * 每次选择最小的放在前面
     * @param arr
     */
    public static void select_sort(int[] arr) {
        int len = arr.length;

        int min = 0;
        for (int i = 0; i < len; i ++) {
             min = i;

            for (int j = i + 1; j < len; j ++) {
                if (arr[j] < arr[min]) {
                    min = j;
                }
            }
            // 交换
            int tmp = arr[min];
            arr[min] = arr[i];
            arr[i] = tmp;

        }


    }


    /**
     * 冒泡
     * 两两比较
     * 每一趟选出一个最大值到后面
     * @param arr
     */
    public static void bubble_sort(int[] arr) {
        int len = arr.length;


        // 冒泡趟数
        for (int i = 0; i < len - 1; i ++) {

            // 也是从0开始
            for (int j = 0; j < len - i - 1; j ++) {
                if (arr[j + 1] < arr[j]) {
                    // 交换
                    int tmp = arr[j + 1];
                    arr[j + 1] = arr[j];
                    arr[j] = tmp;
                }
            }


        }


    }


    /**
     * 插入排序
     * 假设数组前部分是已经排序好的
     * @param arr
     */
    public static void insert_sort(int[] arr) {
        int len = arr.length;


        // 从第二个元素开始,因为默认第一个元素已排序
        for (int i = 1; i < len; i ++) {

            // 一步步与已排序好的数组的最后一个元素进行比较
            // 注意 是arr[j]与arr[j - 1]进行比较
            for (int j =  i; j > 0; j --) {
                if (arr[j] < arr[j - 1]) {
                    // 交换
                    int tmp = arr[j];
                    arr[j] = arr[j - 1];
                    arr[j - 1] = tmp;
                }
            }


        }


    }


    /**
     * 快排
     * 最关键在于i < j
     * @param arr
     * @param start
     * @param end
     */
    public static void quick_sort(int[] arr, int start, int end) {
        // 先判断
        if (start > end) {
            return;
        }

        // 基准数
        int key = arr[start];

        int i = start;
        int j = end;

        while (i < j) {
            // 这里注意都是大于等于小于等于 还有都判断是否i < j
            // 判断i < j 是防止i 移动到j 的后面 或者j 移动到i前面 最多能移动到相邻
            while (arr[j] >= key && i < j) {
                j --;
            }

            while (arr[i] <= key && i < j) {
                i ++;
            }


            // 减少相等时的交换
            // 在交换前无论如何arr[j]基本小于key,除了在i和j的区间已经找不到比key小的值了(这个时候交换好像就不行了?)
            if (i < j && arr[i] != arr[j]) {
                // 交换i和j
                int tmp = arr[i];
                arr[i] = arr[j];
                arr[j] = tmp;
            }


        }

        // 交换key与i值
        arr[start] = arr[i];
        arr[i] = key;

        // 递归之前将数组根据基准值key分为两部分,左边小于基准值,右边大于
        // 递归调用
        quick_sort(arr, start, j - 1);
        quick_sort(arr, j + 1, end);



    }




    /**
     * 二分查找
     * @param arr 已排序的数组
     * @param pos 要查找的数字
     * @return
     */
     public static  int binary_search(int[] arr, int pos) {
         // 先检查是否越界
         if(pos<arr[0] || pos>arr[arr.length-1]){
             return -1;
         }

         int begin = 0;
         int end = arr.length - 1;
         int mid = 0;

         while (begin <= end) {

             mid = (begin + end) / 2;

             if (pos > arr[mid]) {
                 begin = mid + 1;
             } else if (pos < arr[mid]) {
                 end = mid - 1;
             } else {
                 return mid;
             }

         }



         // 默认返回-1即没找到
         return -1;
     }






}

5. 怎样将正整数转为二进制

题目描述

给你一个正整数,不能使用API方法的前提下,编写一个方法将其转为二进制。

思路

面试总结之算法题_第3张图片

package top.hellolsy.offer.others;

/**
 * 将正整数转为二进制
 */
public class IntegerToBinary {

    /**
     * 转换方法
     * @return
     */
    public String transform(int data) {
        StringBuilder buff = new StringBuilder("");

        // 余数
        int result = 0;

        // data每次除2 不断变小,直到为0
        while (data != 0) {
        	// 取余
            result = data % 2;
            buff.append(result);
            data = data / 2;
        }

        // 这个时候是倒序的,需要使用reverse
        System.out.println(buff);


        return buff.reverse().toString();
    }

    public static void main(String[] args) {
        IntegerToBinary integerToBinary = new IntegerToBinary();

        System.out.println("结果为:" +  integerToBinary.transform(30));
    }
}

6. 字符串空格替换

题目描述

请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。

思路

两种方法:

  • 1.常规方法。利用String.charAt(i)以及String.valueOf(char).equals(" “)遍历字符串并判断元素是否为空格。是则替换为”%20",否则不替换
  • 2.使用API的替换方法

当然,这里肯定不会考你使用API的能力,所以使用第一种方法去实现。

package top.hellolsy.offer.string_demo;

/**
 * 字符串空格替换
 * 请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。
 * 两种方法:
 * 1.常规方法。利用String.charAt(i)以及String.valueOf(char).equals(" ")遍历字符串并判断元素是否为空格。是则替换为"%20",否则不替换
 * 2.使用API的替换方法
 *
 * 面试考的话肯定会不是考你用api,所以还是老老实实用第一种去实现吧。
 */
public class ReplaceBlank {



    public static String replaceSpace(StringBuffer str) {
        // 防止多次计算,先缓存下长度值
        int len = str.length();

        // 使用一个StringBuffer作为结果
        StringBuffer result = new StringBuffer();

        for (int i = 0; i < len; i ++) {
            char ch = str.charAt(i);
            // 判断ch是否为空,为空则在result中加入%20
            if (ch == ' ') {
                result.append("%20");
            } else {
                result.append(ch);
            }

        }




        return result.toString();
    }

    public static void main(String[] args) {
        // 测试
        String result = replaceSpace(new StringBuffer("We Are Happy."));

        System.out.println(result);
    }
}

7. 回文字符串问题

题目描述

第一个题目是找出由该字符串中字符组成的最长回文字符串的长度。第二题是验证一个字符串是否是回文字符串。

代码

package top.hellolsy.offer.string_demo;

import java.util.HashSet;

/**
 * 回文字符串相关
 *
 * 1.最长回文串
 *
 *  LeetCode: 给定一个包含大写字母和小写字母的字符串,找到通过这些字母构造成的最长的回文串。
 *  在构造过程中,请注意区分大小写。比如"Aa"不能当做一个回文字符串。
 *  注 意:假设字符串的长度不会超过 1010。
 *
 *  如:
 *  输入:
 * "abccccdd"
 *
 * 输出:
 * 7
 *
 * 解释:
 * 我们可以构造的最长的回文串是"dccaccd", 它的长度是 7。
 *
 * 可以构成回文串的两种情况:
 *
 *     字符出现次数为双数的组合
 *     字符出现次数为双数的组合+一个只出现一次的字符
 *
 * 统计字符出现的次数即可,双数才能构成回文。因为允许中间一个数单独出现,
 * 比如“abcba”,所以如果最后有字母落单,总长度可以加 1。首先将字符串转变为字符数组。
 * 然后遍历该数组,判断对应字符是否在hashset中,
 * 如果不在就加进去,如果在就让count++,然后移除该字符!这样就能找到出现次数为双数的字符个数。
 *
 *
 *
 * 2.验证回文串
 *
 *     LeetCode: 给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。
 *     说明:本题中,我们将空字符串定义为有效的回文串。
 *
 *     如: 输入: "A man, a plan, a canal: Panama"
 * 输出: true
 */
public class PalindromeDemo {

    /**
     * 求出字符串中由其字符组成的最长回文长度
     * @param s
     * @return
     */
    public  int longestPalindrome(String s) {
        if (s.length() == 0) {
            return 0;
        }

        // 存放字符串的set
        HashSet<Character> set = new HashSet<>();
        char[] chars = s.toCharArray();

        // 记录出现两次字母的次数 如aa则为1,aaaa则为2,aabb也为2
        int count = 0;

        for (int i = 0; i < chars.length; i ++) {
            // 如果不包含,则加入到set
            if (!set.contains(chars[i])) {
                set.add(chars[i]);
            } else { // 如果已经有了,说明这是一个双数
                set.remove(chars[i]);
                count ++;
            }
        }

        // 如果set中没有数据了,则回问字符串长度即为双数的个数,否则需要加一
        return set.isEmpty() ? count * 2 : count * 2 + 1;
    }

    /**
     * 验证是否是回文,忽略大小写和标点符号
     * @param s
     * @return
     */
    public  boolean isPalindrome(String s) {
        // 长度为0默认为true
        if (s.length() == 0) {
            return true;
        }

        // 先将标点去除
        StringBuffer buff = new StringBuffer();

        // 去除的效果为:"A man, a plan, a canal: Panama" -> AmanaplanacanalPanama
        for (int i = 0; i < s.length(); i ++) {
            // 使用API进行判断
            // 如果不是标点或空格
            if (Character.isLetterOrDigit(s.charAt(i))) {
                buff.append(s.charAt(i));
            }
        }

        // 再将其转为小写
        String str1  = buff.toString().toLowerCase();

        // 接下来判断是否是回文
        int len = str1.length();
        // 只需要遍历一半
        for (int i = 0; i < len / 2; i ++) {
            // 第一个和最后一个比较,第二个和倒数第二个比较,,,
            if (str1.charAt(i) != str1.charAt(len - i - 1)) {
                return false;
            }
        }

        return true;
    }


    public static void main(String[] args) {
        PalindromeDemo demo = new PalindromeDemo();

//        boolean result = demo.isPalindrome("A man, a plan, a canal: Panama");
        boolean result = demo.isPalindrome("race a car");

        System.out.println(result);
    }
}

8. 字符串转为数字

问题描述

将一个字符串转换成一个整数(实现Integer.valueOf(string)的功能,但是string不符合数字要求时返回0),要求不能使用字符串转换整数的库函数。 数值为0或者字符串不是一个合法的数值则返回0。

思路

先判断第一位是否有"+“或”-",记录下来,然后再遍历后续字符,如果出现非数字,则直接返回0,数字字符则用其减去’0’的ascii码值得到其真实值。

package top.hellolsy.offer.string_demo;

/**
 * 不使用API,将字符串转为数字
 *
 * 将一个字符串转换成一个整数(实现Integer.valueOf(string)的功能,但是string不符合数字要求时返回0),
 * 要求不能使用字符串转换整数的库函数。 数值为0或者字符串不是一个合法的数值则返回0。
 */
public class StringToInteger {

    /**
     * 先判断字符串中是否有非数字的字符(除了第一位可以是+或-以外)
     * 如果没有,再通过ascii计算
     * @param str
     * @return
     */
    public static int StrToInt(String str) {

        // 特殊情况先处理
        if (str.length() == 0) {
            return 0;
        }

        char[] chars = str.toCharArray();

        // 符号位标志,0为默认就是+,1为+,2为-
        int flag = 0;

        if (chars[0] == '+') {
            flag = 1;
        } else if (chars[0] == '-') {
            flag = 2;
        }

        // 如果有符号位,则从1开始,没有则0开始
        int start = flag > 0 ? 1 : 0;

        // 保存结果
        int result = 0;

        for (int i = start; i < chars.length; i ++) {
            // 如果是数字没问题,只要出现一次非数字,则返回0
            if (Character.isDigit(chars[i])) {
                int temp = chars[i] - '0'; // 根据ascii码来计算
                result = result * 10 + temp;

            } else {
                return 0;
            }
        }


        return flag != 2 ? result : -result;
    }
}

9. 其他链表问题整理

说明

这里有关链表的问题,无论是反转链表,合并两个有序链表,删除链表倒数第n个节点,两条链表相加等问题都是使用了一个ArrayList作为容器。

合并两个有序链表

package top.hellolsy.offer.链表相关;


import java.util.ArrayList;
import java.util.Arrays;

/**
 * 合并两个排序的链表
 *
 * 剑指offer:输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。
 *
 * 其他人的思路:
 * 问题分析:
 *
 * 我们可以这样分析:
 *
 *     假设我们有两个链表 A,B;
 *     A的头节点A1的值与B的头结点B1的值比较,假设A1小,则A1为头节点;
 *     A2再和B1比较,假设B1小,则,A1指向B1;
 *     A2再和B2比较 就这样循环往复就行了,应该还算好理解。
 *
 * 考虑通过递归的方式实现!
 *
 *
 * 自己思路:
 * 还是使用数组作为容器吧
 * 将两个链表的数据都存入ArrayList, 然后使用排序算法将其排好,再用for一个个链接即可
 */
public class CombineLikedList {


    public ListNode Merge(ListNode list1,ListNode list2) {
        ListNode result = null;

        ArrayList<Integer> list = new ArrayList<Integer>();

        // 利用数组作为容器,将数据都存入list
        while (list1 != null) {
            list.add(list1.val);
            list1 = list1.next;
        }

        while (list2 != null) {
            list.add(list2.val);
            list2 = list2.next;
        }

        // 再排序
        Integer[] arr = (Integer[]) list.toArray();
        Arrays.sort(arr);

        // 接下来拼接即可
        result = new ListNode(arr[0]);
        for (int i = 1; i < arr.length; i ++) {
            // 再循环遍历出node的最后一个节点,并拼接进去

            ListNode tmp = result;
            while (true) {
                if (tmp.next == null) {
                    tmp.next = new ListNode(arr[i]);
                    break;
                }
                tmp = tmp.next;

            }
        }


        return result;
    }


}

两数相加

package top.hellolsy.offer.链表相关.q2;

import top.hellolsy.offer.链表相关.ListNode;

/**
 *
 * 两数相加 返回两个链表的val的值相加的链表
 *
 *

 Leetcode:给定两个非空链表来表示两个非负整数。位数按照逆序方式存储,它们的每个节点只存储单个数字。将两数相加返回一个新的链表。

 你可以假设除了数字 0 之外,这两个数字都不会以零开头。
 * 如
 * 1->2->3->4
 * +
 * 2->8->4->5
 * =
 * 3->0->8->9
 *
 *
 * 两次遍历
 * 第一次遍历:两个链表对应每个节点分别取和,若含有空节点则空节点取0,产生一个新链表。
 * 第二次遍历:对取完和的新链表遍历,判断当前的val是否大于等于10,大于或等于则其自身-10其next加1,若next为空则新建0节点。
 */
public class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        ListNode rs = new ListNode(l1.val + l2.val);

        l1 = l1.next;
        l2 = l2.next;
        ListNode temp = rs;
        while (l1 != null || l2 != null) {
            int a = 0;
            int b = 0;
            if (l1 != null) {
                a = l1.val;
            }
            if (l2 != null) {
                b = l2.val;
            }

            int t = a + b;
            temp.next = new ListNode(t);
            temp = temp.next;
            if (l1 != null) {
                l1 = l1.next;
            }
            if (l2 != null) {
                l2 = l2.next;
            }
        }

        temp = rs;
        while (temp != null) {
            if (temp.val >= 10) {
                temp.val = temp.val - 10;
                if (temp.next == null) {
                    temp.next = new ListNode(0);
                }
                temp.next.val = temp.next.val + 1;
            }
            temp = temp.next;
        }

        return rs;
    }

    public static void main(String[] args) {
        Solution solution = new Solution();

        ListNode l1 = new ListNode(1);

//        solution.addTwoNumbers();
    }
}

删除链表的倒数第N个节点

package top.hellolsy.offer.链表相关.q19;

import top.hellolsy.offer.链表相关.ListNode;

import java.util.ArrayList;
import java.util.Stack;

/**
 * 利用两个指针 o(n)
 *
 * 删除链表的倒数第N个节点
 * Given linked list: 1->2->3->4->5, and n = 2.
 *
 * After removing the second node from the end, the linked list becomes 1->2->3->5.
 */
public class Solution {

    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummy = new ListNode(0);
        dummy.next = head;
        ListNode first = dummy;
        ListNode second = dummy;

        for (int i = 1; i <= n + 1; i++) {
            first = first.next;
        }

        while (first != null) {
            first = first.next;
            second = second.next;
        }
        second.next = second.next.next;
        return dummy.next;
    }


    /**
     * 利用其他方法完成
     * @param head
     * @param n
     * @return
     */
    public ListNode removeNthFromEnd1(ListNode head, int n) {
        ListNode result = null;

        // 先全部存入数组
        ArrayList<Integer> list = new ArrayList();

        while (head != null) {
            list.add(head.val);
            head = head.next;
        }

        // 越界
        if (n <= 0 || n > list.size()) {
            return null;
        }

        // 再剔除倒数第n个
        list.remove(list.size() - n);

        // 再链接为一个链表
        int len = list.size();
        result = new ListNode(list.get(0));
        for (int i = 1; i < len; i ++) {

            // 再循环遍历出node的最后一个节点,并拼接进去
            ListNode tmp = result;
            while (true) {
                if (tmp.next == null) {
                    tmp.next = new ListNode(list.get(i));
                    break;
                }
                tmp = tmp.next;

            }
        }


        return result;
    }

}

10. 斐波那契系列

题目描述

这里会分享三个和斐波那契有关的题目,第一个是求斐波那契数列第n个位置的数字,第二个题目是青蛙跳台阶题目,第三个是变态跳台阶问题。

思路

package top.hellolsy.offer.feiqinabo;

/**
 * 斐波那契系列题目
 *
 * 1. 斐波那契数列
 * 0、1、1、2、3、5、8、13、21、34...
 * 可以使用F(1)=1,F(2)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 3,n ∈ N*)递推
 * 解法:第一个最简单的递归,但容易出现栈溢出,还有重复计算如果过多,也有可能导致OOM
 * 第二个迭代,利用变量存储之前的值,就不会导致重复计算
 * 第三个动态规划,利用数组存储之前的值,避免重复计算 使用动态规划,数组保存每一个值数据,只需要计算一遍
 *
 * 2.跳台阶问题
 * 一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
 *
 * 和斐波那契规律一样,不过初始数字为 1 2 3 5...
 *
 * 3.变态跳台阶问题
 * 一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
 *
 * 假设n>=2,第一步有n种跳法:跳1级、跳2级、到跳n级 跳1级,剩下n-1级,则剩下跳法是f(n-1) 跳2级,剩下n-2级,则剩下跳法是f(n-2) ...... 跳n-1级,剩下1级,则剩下跳法是f(1) 跳n级,剩下0级,则剩下跳法是f(0)
 * 所以在n>=2的情况下: f(n)=f(n-1)+f(n-2)+...+f(1) 因为f(n-1)=f(n-2)+f(n-3)+...+f(1) 所以f(n)=2f(n-1) 又f(1)=1,所以可得*f(n)=2^(number-1)**
 *
 *
 *
 */
public class FibonacciDemo {

    /**
     * 输出第n个位置上的斐波那契值
     * 先使用递归
     * @param n
     * @return
     */
    public int Fibonacci(int n) {
        // 越界
        if (n < 0) {
            return -1;
        }

        if (n < 2) {
            return n;
        }

        return Fibonacci(n - 1) + Fibonacci(n - 2);

    }

    /**
     * 使用迭代
     * @param n
     * @return
     */
    public int Fibonacci1(int n) {

        if (n <= 0) {
            return 0;
        }

        if (n == 1 || n == 2) {
            return 1;
        }

        // 两个值的和
        int count = 0;
        int first = 1;
        int second = 1;

        for (int i = 3; i <= n; i ++) {
            count = first + second;
            first = second;
            second = count;
        }

        return count;

    }

    /**
     * 青蛙跳台阶
     * 使用迭代
     * @param n
     * @return
     */
    public int jumpFloor(int n) {
        if (n <= 0) {
            return 0;
        }

        if (n == 1 || n == 2) {
            return n;
        }

        // 两个值的和
        int count = 0;
        int first = 1;
        int second = 2;

        for (int i = 3; i <= n; i ++) {
            count = first + second;
            first = second;
            second = count;
        }

        return count;

    }

    /**
     * 变态青蛙跳
     * f(n) = 2*f(n-1)
     * @param n
     * @return
     */
    public int JumpFloorII(int n) {

        return 1 << (n - 1); //2^(number-1)用位移操作进行,更快
    }



    /**
     * 测试
     * @param args
     */
    public static void main(String[] args) {
        FibonacciDemo demo = new FibonacciDemo();

        int result = demo.Fibonacci1(6);

        System.out.println(result);

    }






}

11. 二维数组查找

题目描述

在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。 请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

思路

package top.hellolsy.offer.others;

/**
 * 二维数组查找
 *
 * 在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。
 * 请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
 *
 * 思路是从右上角开始 是一个中间值
 */
public class SearchInArr {

    public boolean Find(int target, int [][] array) {

        // 多少列
        int xLen = array[0].length;
        // 多少行
        int yLen = array.length;

        if (array[0][xLen - 1] == target) {
            return true;
        }

        int i = 0;
        int j = xLen - 1;
        int start = array[i][j];
        while (i < yLen && j >= 0) {
            if (target > start) {
                i ++;
            } else if (target < start) {
                j --;
            } else {
                return true;
            }



        }


        return false;
    }
}

12. 二叉树遍历

题目概述

接下来回顾二叉树的前中后序及层序遍历。

思路

package top.hellolsy.offer.tree;

import com.sun.jmx.remote.internal.ArrayQueue;
import sun.reflect.generics.tree.Tree;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;

/**
 * 二叉树的中序,前序,后序,层序遍历
 */
public class SearchInTree {

    private List<Integer> result = new ArrayList<>();


    public static void main(String[] args) {
        TreeNode root = new TreeNode(1);
        TreeNode t1 = new TreeNode(2);
        root.right = t1;
        TreeNode t2 = new TreeNode(3);
        root.left = t2;
        SearchInTree demo = new SearchInTree();
//        demo.preorderTraversal(root);
        demo.inorderTraversal(root);
//        demo.postorderTraversal(root);
        System.out.println(demo.result);
    }



    /**----------递归版本----------**/
    /**
     * 中序遍历
     * 根节点中间遍历
     *
     * 使用递归,简单很多
     * @param root
     * @return
     */
    public void inorderTraversal(TreeNode root) {

        if (root.left != null) {
            inorderTraversal(root.left);
        }

        result.add(root.val);

        if (root.right != null) {
             inorderTraversal(root.right);
        }

    }

    /**
     * 前序遍历递归版
     * @param root
     * @return
     */
    public void preorderTraversal(TreeNode root) {
        result.add(root.val);

        if (root.left != null) {
            preorderTraversal(root.left);
        }

        if (root.right != null) {
            preorderTraversal(root.right);
        }
    }

    /**
     * 后序遍历递归版
     * @param root
     * @return
     */
    public void postorderTraversal(TreeNode root) {

        if (root.left != null) {
            postorderTraversal(root.left);
        }

        if (root.right != null) {
            postorderTraversal(root.right);
        }

        result.add(root.val);
    }

    /**----------
     * 非递归版本
     * 迭代方法去做,需要自己模拟栈,即使用栈作为辅助
     * ----------**/

    /**
     * 中序遍历
     * @param root
     */
    public void inorderTraversal1(TreeNode root) {



    }


    /**
     * 利用深度优先搜索实现层序遍历
     * @param root
     */
    public void bfs(TreeNode root) {

        // 队列
        Queue<TreeNode> queue = new ArrayDeque<>();
        queue.add(root);

        while (!queue.isEmpty()) {
            // 弹出
            TreeNode node = queue.poll();
            result.add(node.val);

            if (node.left != null) {
                queue.add(node.left);
            }
            if (node.right != null) {
                queue.add(node.right);
            }


        }


    }



}

13. 两个栈模拟队列

题目描述

使用两个栈实现一个队列,实现其先进先出的效果。

思路

package top.hellolsy.offer.stack;


import java.util.Stack;

/**
 * 使用两个栈实现一个队列
 * 当push的时候将元素push进stack1,pop的时候我们先把stack1的元素pop到stack2,
 * 然后再对stack2执行pop操作,这样就可以保证是先进先出的。(负[pop]负[pop]得正[先进先出])
 */
public class StackDemo {

    private Stack<Integer> stack1 = new Stack<>();
    private Stack<Integer> stack2 = new Stack<>();

    // 加入队列
    public void push(int data) {
        // 插入stack1
        stack1.push(data);
    }

    // 出队列
    public int pop() {

        // 先判空
        if(stack1.empty()&&stack2.empty()){
            throw new RuntimeException("Queue is empty!");
        }

        // 要使用两个stack保证先进先出

        // 先将stack1的数据移到stack2
        // 如果stack2中已经有数据了则先将stack2中国数据用完
        if (stack2.empty()) {
            while (!stack1.empty()) {
                stack2.push(stack1.pop());
            }

        }

        // 再从stack2取数据
        return stack2.pop();

    }


    public static void main(String[] args) {
        StackDemo demo = new StackDemo();

        demo.push(3);
        demo.push(4);
        demo.push(5);
        demo.push(6);

        System.out.println(demo.pop());
        System.out.println(demo.pop());
        System.out.println(demo.pop());
        System.out.println(demo.pop());


    }


}

14. 两个数组的交集

题目描述

给定两个数组,编写一个函数来计算它们的交集。

思路

面试总结之算法题_第4张图片
利用map进行存储,先将第一个数组的值全存储到map中,key是数据,value是对应数值出现的次数。然后遍历第二个数组过程中,先判断遍历的数值在map中是否存在,如果存在再判断其value是否大于0,如果是则先将map中该key对应的value - 1,然后将此key存入result结果集。看代码可能好理解些。

package top.hellolsy.offer.array;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

/**
 * 求两个数组的交集
 */
public class Intersection {

    public int[] intersect(int[] arr1, int[] arr2) {
        HashMap<Integer, Integer> map = new HashMap<>();
        ArrayList<Integer> list = new ArrayList<>();
        // 第一种想法,遍历,第二种,使用map存储 value就是出现的次数
        // 先将arr1的全弄到map key就是其中的字符,而value就是出现的次数
        for (int i : arr1) {
            if (map.containsKey(i)) {
                map.put(i, map.get(i) + 1);
            } else {
                map.put(i, 1);
            }
        }

        // arr2中的元素如果已经再map中了,则直接存储到结果集并且map中对应字符的value - 1
        for (int i : arr2) {
            if (map.containsKey(i)) {

                // 等于0后代表第一个数组没有这个元素了
                if (map.get(i) > 0) {
                    map.put(i, map.get(i) - 1);
                    list.add(i);
                }
            }

        }

        int[] result = new int[list.size()];
       // 循环赋值 或者直接使用转换方法(list.toArray(result) 缺点是只能是包装类的数组,而不是int[])
        for (int i = 0; i < list.size(); i ++) {
            result[i] = list.get(i);
        }

        return result;
    }


    public static void main(String[] args) {
        int[] arr1 = new int[]{1,2,2,1,7,8,9,8};
        int[] arr2 = new int[]{2,2,1,4,5,68,8};

        Intersection intersection = new Intersection();
        int[] result = intersection.intersect(arr1, arr2);

        System.out.println(Arrays.toString(result));


    }
}

15. 最长公共前缀

题目描述

编写一个函数来查找字符串数组中的最长公共前缀。如果不存在公共前缀,则返回""。

思路

我这里编写了两个函数,一个是find(String str1, String str2),这个是求两个字符串的最长公共前缀,是这个题目的简化版,利用这个思路编写出题解的longestCommonPrefix()方法。具体思路看代码及注释。

package top.hellolsy.offer.array;

/**
 * 求最长公共前缀
 *
 * 编写一个函数来查找字符串数组中的最长公共前缀。如果不存在公共前缀,则返回""
 *
 * 例子:
 * 输入: ["flower","flow","flight"]
 * 输出: "fl"
 */
public class LongestCommonPrefix {


    /**
     * 最长公共前缀
     * @param strs
     * @return
     */
    public String longestCommonPrefix(String[] strs) {
        // 将第一个元素作为基准元素,与数组元素不断比较,不断减少
        // 原理与find方法类似
        String str = strs[0];

        // 是否完全匹配的标志
        int flag = 0;
        while (!"".equals(str)) {

            // 不断拿修改后的str与数组中元素一一比较。
            for (String s : strs) {
                // 只要不符合,就减少一个字符
                if (!s.startsWith(str)) {
                    str = str.substring(0, str.length() - 1);
                    flag ++;
                }
            }

            // 说明没被修改,即全部匹配
            if (flag == 0) {
                return str;
            }

            // 数据还原
            flag = 0;

        }


        return "";
    }


    /**
     * 找到两个字符串的最长公共前缀
     * @param str1
     * @param str2
     * @return
     */
    public String find(String str1, String str2) {
        // 先将其中的较短的弄出来
        String min = (str1.length() > str2.length()) ? str2 : str1;
        String max = (str1.length() > str2.length()) ? str1 : str2;

        while (!"".equals(min)) {
            // 每次都减少一个字符

            // 如果包含说明找到了
            if (max.startsWith(min)) {
                return min;
            }

            // 否则没找到就减少一个字符
            min = min.substring(0, min.length() - 1);
        }

        // 实在找不到则返回空
        return "";
    }


    public static void main(String[] args) {
        LongestCommonPrefix longestCommonPrefix = new LongestCommonPrefix();

        System.out.println(longestCommonPrefix.find("low", "loedd"));


        String[] strs = new String[] {"flower","flow","flight"};
        System.out.println(longestCommonPrefix.longestCommonPrefix(strs));
    }


}


设计模式类型

抽象工厂

简介

抽象工厂模式创建的是对象家族,也就是很多对象而不是一个对象,并且这些对象是相关的,也就是说必须一起创建出来。而工厂方法模式只是用于创建一个对象,这和抽象工厂模式有很大不同。

抽象工厂模式用到了工厂方法模式来创建单一对象,AbstractFactory 中的 createProductA() 和 createProductB() 方法都是让子类来实现,这两个方法单独来看就是在创建一个对象,这符合工厂方法模式的定义。

至于创建对象的家族这一概念是在 Client 体现,Client 要通过 AbstractFactory 同时调用两个方法来创建出两个对象,在这里这两个对象就有很大的相关性,Client 需要同时创建出这两个对象。

从高层次来看,抽象工厂使用了组合,即 Cilent 组合了 AbstractFactory,而工厂方法模式使用了继承。
面试总结之算法题_第5张图片

package top.hellolsy.offer.design_mode;

/**
 * 抽象工厂模式
 */
public class Client{

    public static void main(String[] args) {
        // 海南工厂
        AbstractFactory factory = new HaiNanFactory();
        Refrigerator refrigerator = factory.createRefrigerator();
        Conditioner conditioner = factory.createConditioner();

        refrigerator.reFresh();
        conditioner.freeze();

        System.out.println("-------------分割线---------------");

        // 上海工厂
        factory = new ShangHaiFactory();
        refrigerator = factory.createRefrigerator();
        conditioner = factory.createConditioner();

        refrigerator.reFresh();
        conditioner.freeze();


    }


}


/**
 * 抽象工厂类
 */
abstract class AbstractFactory {

    abstract Refrigerator createRefrigerator();
    abstract Conditioner createConditioner();

}


/**
 * 具体工厂->海南电子厂
 */
class HaiNanFactory extends AbstractFactory {

    @Override
    Refrigerator createRefrigerator() {
        return new Haier_Refrigerator();
    }

    @Override
    Conditioner createConditioner() {
        return new Geli_Conditioner();
    }
}

/**
 * 具体工厂->上海电子厂
 */
class ShangHaiFactory extends AbstractFactory {

    @Override
    Refrigerator createRefrigerator() {
        return new XiMenZi_Refrigerator();
    }

    @Override
    Conditioner createConditioner() {
        return new Aux_Conditioner();
    }

}

/**
 * 抽象冰箱产品类
 */
abstract class Refrigerator {
    // 保鲜
    public abstract void reFresh();
}

/**
 * 抽象空调产品类
 */
abstract class Conditioner {
    // 制冷
    public abstract void freeze();
}

/**
 * 具体冰箱产品类:海尔冰箱
 */
class Haier_Refrigerator extends Refrigerator  {

    @Override
    public void reFresh() {
        System.out.println("海尔冰箱保鲜,用了不会后悔...");
    }
}

/**
 * 具体冰箱产品类:西门子冰箱
 */
class XiMenZi_Refrigerator extends Refrigerator  {

    @Override
    public void reFresh() {
        System.out.println("西门子冰箱保鲜,用了不会后悔...");
    }
}


/**
 * 具体空调产品类:奥克斯空调
 */
class Aux_Conditioner extends Conditioner {

    @Override
    public void freeze() {
        System.out.println("奥克斯空调制冷,让你从早凉到晚...");
    }
}

/**
 * 具体产品类:格力空调
 */
class Geli_Conditioner extends Conditioner {

    @Override
    public void freeze() {
        System.out.println("格力空调制冷,让你从早凉到晚...");
    }
}






静态、动态代理手写

静态代理

package top.hellolsy.offer.design_mode;

/**
 * 静态代理
 */
public class Proxy_static_demo {

    public static void main(String[] args) {

        Demo_static demo = new Teacher();

        Proxy proxy = new Proxy(demo);

        proxy.show();

    }
}

/**
 * 被代理类实现的接口
 */
interface Demo_static {

    public void show();
}


/**
 * 一个老师
 * 被代理对象
 */
class Teacher  implements Demo_static{

    @Override
    public void show() {
        System.out.println("认真的讲课中...");
    }
}

/**
 * 静态代理类需要实现和被代理对象一样的接口,这样可以获得一样的方法
 * 外界可以访问这个相同方法
 */
class Proxy implements Demo_static {

    // 被代理对象
    private Demo_static demo;

    public Proxy (Demo_static demo) {
        this.demo = demo;
    }


    @Override
    public void show() {
        System.out.println("上课前先跳个舞...");

        // 被代理对象的方法
        demo.show();

        System.out.println("下课后和孩子一起玩耍...");

    }
}



面试总结之算法题_第6张图片

动态代理

package top.hellolsy.offer.design_mode;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * 动态代理
 * 利用JDK动态代理展示下
 * 基于JDK的动态代理就需要知道两个类:1.InvocationHandler(接口)、2.Proxy(类)
 * 且JDK的动态代理是基于被代理类实现了接口
 */
public class Proxy_dymatic_demo {


    public static void main(String[] args) {
        Demo_dymatic demo = new Teacher_();
        InvocationHandler invocationHandler = new TeacherProxy_(demo);
        // 传入被代理类的类加载器,实现的接口,代理类自身
        Demo_dymatic teacher_proxy = (Demo_dymatic) Proxy.newProxyInstance(demo.getClass().getClassLoader(),
                demo.getClass().getInterfaces(), invocationHandler);

        teacher_proxy.show();

    }
}


/**
 * 被代理类实现的接口
 */
interface Demo_dymatic {

    public void show();
}


/**
 * 一个老师
 * 被代理类
 */
class Teacher_  implements Demo_dymatic{

    @Override
    public void show() {
        System.out.println("认真的讲课中...");
    }
}

/**
 * 代理类
 */
class TeacherProxy_ implements InvocationHandler {

    // 被代理对象
    private Object demo;

    public TeacherProxy_ (Demo_dymatic demo) {
        this.demo = demo;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("上课前先跳个舞...");

        // 执行被代理对象的方法
        // 利用反射调用
        Object Invoke = method.invoke(demo,args);

        System.out.println("下课后和孩子一起玩耍...");

        return Invoke;
    }
}

SQL编写

你可能感兴趣的:(面试总结,算法,算法,面试,数据结构,链表)