常见面试算法题汇总 剑指offer(在线编程) 剑指offer(案例总结)
1、单例
饿汉式是类一加载进内存就创建好了对象;
//饿汉式
class Singleton {
private Singleton() {}
private static Singleton s = new Singleton();
public static Singleton getInstance() {
return s;
}
}
懒汉式是类加载进内存的时候,对象还没有存在,要使用该对象的时候再new出来,*懒汉式是延迟加载,如果多个线程同时操作懒汉式时就有可能出现线程安全问题 为解决饱汉式的线程安全问题,解决线程安全问题可以加同步来解决。但是加了同步之后,每一次都要比较锁,效率就变慢了。
//懒汉式
class Single {
private Single() {}
private static Single s = null ;
public static Single getInstance() {
public static synchronized Single getInstance() { //加锁同步安全
if (s == null) {
s = new Single();
}
return s;
} }
通过双重判断来提高程序效率,所谓“双重检查加锁”机制,指的是:
//双重检查
class Single {
private Single() {}
private volatile static Single s = null;
public static Single getInstance() {
//先检查实例是否存在,如果不存在才进入下面的同步块
if (s == null) {
//同步快,线程安全创建实例
synchronized (Singleton.class) {
//再次检查实例是否存在,如果不存在才真正的创建实例
if(s == null){
s = new Single();
}
}
}
return s;
} }
1、第一个条件是说,如果实例创建了,那就不需要同步了,直接返回就好了。 if
(singleton==
null
) {
2、不然,我们就开始同步线程。
synchronized
(Singleton.
class
) {
3、第二个条件是说,如果被同步的线程中有一个线程创建了对象,那么别的线程就不用再创建了。
if
(singleton==
null
) {
问题主要在于singleton = new Singleton()
这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情
1、给 singleton 分配内存
2、调用 Singleton 的构造函数来初始化成员变量,形成实例
3、将singleton对象指向分配的内存空间(执行完这步 singleton才是非 null 了)
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错
对此,我们只需要把singleton声明成 volatile 就可以了,有两个功用:
1)这个变量不会在多个线程中存在复本,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量
2)这个关键字会禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。
静态内部类:
支持多线程-懒汉式
//特点:利用静态内部类(只有在出现它的引用时才被加载),完成懒加载
class Single{
private Single(){}
public static Single getInstance(){
return SingleHolder.instance;
}
private static class SingleHolder{
public static Single instance = new Single();
}
}
2、回文
“回文串”是一个正读和反读都一样的字符串,如“level”或者“noon”等就是回文串。
第一种
String str = "1234321";
StringBuilder sb = new StringBuilder(str);
String newStr = sb.reverse().toString();
if(newStr.equals(str)){
System.out.println(str+"是回文字符串");
}else {
System.out.println(str+"不是是回文字符串");
}
第二种
String str = "1234321";
int count = 0;
for (int i = 0; i < str.length() / 2; i++) {
if ((str.substring(i, i + 1)).equals(str.substring(str.length() - 1- i, str.length() - i))) {
count++;
}
}
if (count == str.length() / 2) {
System.out.println("是回文字符串");
}else{
System.out.println("不是回文字符串");
}
第三种
public static boolean palindromeNumber(int num) {
int div = 1;
while(num / div >= 10){
div *= 10;
}
while(num > 0){
if(num / div != num % 10){
return false;
}
num = (num % div) / 10;
div /= 100;
}
return true;
}
3、翻转字符串
翻转单词顺序列,如输入“student. a am I”,输出“I am a student.”
String str ="student a am I";
if(str.trim().equals("") || str == null){
return str;
}
String [] c = str.split(" ");
StringBuffer sb = new StringBuffer();
for(int i = c.length;i>0;i--){
sb.append(c[i-1]);
if(i>1){
sb.append(" ");
}
}
return sb.toString();
翻转一个整数 例如 22334455
public static int reverseInteger(int n) {
int num = 0;
while (n != 0) {
int temp = num * 10 + n % 10;
n = n / 10;
if (temp / 10 != num) {
num = 0;
break;
}
num = temp;
}
return num;
}
4、线程
(1)用三个线程,顺序打印字母A-Z,输出结果是1A、2B、3C、1D 2E...
static char c = 'A';
static int i = 0;
public static void main(String[] args) {
Runnable runnable = new Runnable() {
public void run() {
synchronized (this) {//加锁
try {
int threadId = Integer.parseInt(Thread.currentThread().getName());
while (i < 26) {
if (i % 3 == threadId - 1) {
System.out.println(threadId +" "+ (char) c++);
i++;
notifyAll();// 唤醒处于等待状态的线程
} else {
wait();// 释放当前锁并进入等待状态
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}//执行结束释放当前锁
}
};
Thread t1 = new Thread(runnable, "1");
Thread t2 = new Thread(runnable, "2");
Thread t3 = new Thread(runnable, "3");
t1.start();
t2.start();
t3.start();
}
(2)多线程以及锁机制
创建2个线程共享10张票,在while()循环中添加sleep()方法,由于线程有延迟,出现了票数为负数的情况,此时需要同步锁机制,使用synchronized同步代码块,共享资源的代码只能有一个线程访问。
public static void main(String [] args){
Ticket ticket = new Ticket();
new Thread(ticket,"1").start();
new Thread(ticket,"2").start();
}
static class Ticket implements Runnable{
private int tickets = 10;
//锁对象是任意类型,唯一的。不能放到run方法中,否则每次创建一个新对象有不同的锁,无法同步。
Object lock = new Object();
public void run() {
while (true){
synchronized (lock){ //同步代码块
if(tickets>0){
try {
Thread.sleep(10); //休眠10秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(tickets--);
}else {
break; //跳出循环,终止程序
}
}
}
}
}
(3)、死锁
创建了china线程和Ameri线程,china线程中拥有chop锁,只有获得knife锁才能执行完毕,而Ameri线程中拥有knife锁,只有获得chop锁才能执行完毕,两个线程需要对方所占用的锁,但是无法释放自己拥有的锁,全都处于挂起状态。
public class DeadLockThread {
public static void main(String [] args){
new Thread(new DeadLock(true),"china").start();;
new Thread(new DeadLock(false),"Ameri").start();
}
}
class DeadLock implements Runnable{
//定义chop和knife两种锁对象
Object chop = new Object();
Object knife = new Object();
boolean flag;
public DeadLock(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if(flag){
while (true){
synchronized (chop){
System.out.println(Thread.currentThread().getName()+"----chop");
synchronized (knife){
System.out.println(Thread.currentThread().getName()+"----knife");
}
}
}
}else {
while (true){
synchronized (knife){
System.out.println(Thread.currentThread().getName()+"----knife");
synchronized (chop){
System.out.println(Thread.currentThread().getName()+"----chop");
}
}
}
}
}
}
5、斐波拉契数列介绍
斐波那契数列(Fibonacci sequence),又称黄金分割数列。斐波那契数列源自于兔子繁殖问题,是数学家列昂纳多·斐波那契引入的,故又称为“兔子数列”,指的是这样一个数列:1、1、2、3、5、8、13、21、34、……
数列中的每1项称为“斐波那契数”,斐波那契数的前两项都是1,从第3项开始,每1项都等于前2项之和。
a[n]=a[n-1]+a[n-2]
1、利用循环的方式
//声明三个变量
int a=1,b=1,c=0;
// 从第三项开始,每个数字都都等于前面两个数字之和
for (int i = 3; i <=10 ; i++) {
c=a+b;
a= b;
b=c;
System.out.println(c);
}
2、利用递归的方式
public static int computer(int num){
if(num == 1 || num ==2){
return 1;
}
return computer(num-1)+computer(num-2);
}
6、递归和循环的比较:
1、递归慢的原因:
递归的实现是通过调用函数本身,函数调用的时候,每次调用时要做地址保存,参数传递等,这是通过一个递归工作栈实现的。具体是每次调用函数本身要保存的内容包括:局部变量、形参、调用函数地址、返回值。那么,如果递归调用N次,就要分配N*局部变量、N*形参、N*调用函数地址、N*返回值。
2、用循环效率会比递归效率高吗?
并不是说循环效率就一定比递归高,递归和循环是两码事,递归带有栈操作,循环则不一定
递归优点:代码简洁、清晰,并且容易验证正确性
缺点:运行需要多次数函数调用,如果调用层数比较深,需要增加额外的堆栈处理(还有可能出现堆栈溢出的情况),比如参数传递需要压栈等操作,会对执行效率有一定影响。
循环优点:速度快,结构简单
缺点:并不能解决所有的问题。有的问题适合使用递归而不是循环。如果使用循环并不困难的话,最好使用循环
7、二叉树
class TreeMode{
public TreeMode left,right;
public int val;
public TreeMode(int val){
this.val = val;
}
}
//先序 根左右
public void preTraverse(TreeMode root){
if(root != null){
System.out.println(root.val);
preTraverse(root.left);
preTraverse(root.right);
}
}
//中序 左根右
public void inTraverse(TreeMode root){
if(root !=null){
inTraverse(root.left);
System.out.println(root.val);
inTraverse(root.right);
}
}
//后续 左右根
public void postTraverse(TreeMode root){
if(root !=null){
postTraverse(root.left);
postTraverse(root.right);
System.out.println(root.val);
}
}
//左右翻转
public void invert(TreeMode root){
TreeMode temp = root.left;
root.left = root.right;
root.right = temp;
invert(root.left);
invert(root.right);
}
//最大值
public int getMax(TreeMode root){
if(root == null){
return Integer.MIN_VALUE;
}else {
int left = getMax(root.left);
int right = getMax(root.right);
return Math.max(Math.max(left,right),root.val);
}
}
//最大深度
public int maxDepth(TreeMode root){
if(root == null){
return 0;
}
int left = maxDepth(root.left);
int right = maxDepth(root.right);
return Math.max(left,right)+1;
}
//最小深度
public int minDepth(TreeMode root){
if(root == null){
return 0;
}
int left = minDepth(root.left);
int right = minDepth(root.right);
if(left == 0){
return right+1;
}else if(right == 0){
return left+1;
}else {
return Math.min(left,right)+1;
}
}
8、链表
public class ListNode {
int val;
ListNode next ;
ListNode(int x) {
val = x;
next = null;
}
}
删除节点
public void deleteNode(ListNode node) {
if (node.next == null){
node = null;
return;
}
// 取缔下一节点
node.val = node.next.val
node.next = node.next.next
}
翻转链表
方法二: 递归,使链表从尾节点开始指向前一个节点
public ListNode reverseLinkedList(ListNode head){
if (head == null || head.next == null){
return head;
}
ListNode pNode = reverseLinkedList(head.next);
head.next.next = head;
head.next = null;
return pNode;
}
判断链表是否有环
public static boolean containCircle(Node head) {
Node fastPoint = head;
Node slowPoint = head;
if (fastPoint == null) {
return false;
}
while (fastPoint.next != null) {
fastPoint = fastPoint.next.next;
slowPoint = slowPoint.next;
if (fastPoint == slowPoint) {
return true;
}
}
return false;
}
/**
* 判断链表是否为回文链表
*/
public static boolean isPalindrome(Node head) {
if (head == null || head.next == null) {
return true;
}
Node quick = head;
Node slow = head;
//快慢指针,快指针一次走两步,慢指针一次走一步
while (quick != null && quick.next != null) {
quick = quick.next.next;
slow = slow.next;
}
//从slow开始反转后半段链表
Node pre = null;
while (slow != null) {
Node temp = slow.next;
slow.next = pre;
pre = slow;
slow = temp;
}
//对比前半段和后半段的data值是否相同
while (pre != null) {
if (pre.val == head.val) {
pre = pre.next;
head = head.next;
} else {
return false;
}
}
//返回true
return true;
}
//链表每k个节点一组进行翻转,如果节点总数不是k的整数倍保持原有顺序。
public ListNode reversekGroup(ListNode head,int k){
if(head == null || head.next == null || k==1){
return head;
}
//寻找第k个节点
ListNode kNode = head;
for (int i = 0; i < k && kNode!= null; i++) {
kNode = head.next;
}
//若不够k个节点不用翻转直接返回head
if(kNode == null){
return head;
}
//若够k个节点,则翻转前k个节点,把k+1个节点记录下来
kNode = kNode.next;
head.next = reversekGroup(kNode,k);
ListNode pNode = reverse(head,k);
return pNode;
}
ListNode reverse(ListNode head, int k) {
ListNode pNode = reverse(head.next,k-1);
head.next.next = head;
head.next = null;
return pNode;
}
//链表每k个节点一组进行翻转,如果节点总数不是k的整数倍,剩余的直接翻转
public ListNode reversekGroup(ListNode head,int k){
if(head == null || head.next == null || k==1){
return head;
}
//寻找第k个节点
ListNode kNode = head;
for (int i = 0; i < k && kNode!= null; i++) {
kNode = head.next;
}
//链表每k个节点一组进行翻转,如果节点总数不是k的整数倍,剩余的直接翻转。
kNode = kNode.next;
if(kNode == null){
head.next = null;
}else {
head.next = reversekGroup(kNode,k);
}
ListNode pNode = reverse(head,k);
return pNode;
}
ListNode reverse(ListNode head, int k) {
ListNode pNode = reverse(head.next,k-1);
head.next.next = head;
head.next = null;
return pNode;
}
9、栈和队列
用两个栈实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型
- 入队:将元素进栈A
-
出队:判断栈B是否为空,如果为空,则将栈A中所有元素pop,并push进栈B,栈B出栈, 反之栈B直接出栈
public class Solution{
Stack<Integer> stack1 = new Stack<Integer>();
Stack<Integer> stack2 = new Stack<Integer>();
//入队
public void add(int node){
stack1.push(node);
}
//出队
public int poll(){
if (stack1.empty(0 && stack2.empty())){
throw new RuntimeException("Queue is empty!");
}
if (stack2.empty()){
while (!stack1.empty()){
stack2.push(stack1.pop());
}
}
return stack2.pop();
}
}
10、二分搜索
了解哪些查找算法,时间复杂度都是多少?

//递归的方式实现--二分查找
public int binarySearch(int[] arr, int start, end, int hkey){
int mid = start + (end - start) / 2; //防止溢位
if (arr[mid] > hkey) {
return binarySearch(arr, start, mid - 1, hkey);
}
if (arr[mid] < hkey) {
return binarySearch(arr, mid + 1, end, hkey);
}
return mid;
}
//循环的方式实现
private static int binarySearch(int[] array, int hkey) {
int left = 0;
int right = array.length - 1;
int mid = (left + right) / 2;
if (hkey < array[mid]) {
right = mid - 1;
} else if (aim > array[mid]) {
left = mid + 1;
} else {
return mid;
}
}
11、数组
删除元素
删除排序数组中的重复数字
在原数组中“删除”重复出现的数字,使得每个元素只出现一次,并且返回“新”数组的长度,新的数组
(第1种方法)
public class removeElem {
public static void main(String[] args){
Object[] arr = {"a","b","b","c","d"};
Object[] objects = ifRepeat(arr);
System.out.println(objects.length);
System.out.println(objects);
}
//list.contains()
public static Object[] ifRepeat(Object[] arr){
List list = new ArrayList();
for(int i=0;i<arr.length;i++){
if(!list.contains(arr[i])){
list.add(arr[i]);
}
}
Object[] newArr = list.toArray();
return newArr;
}
//HashSet()
public static Object[] ifRepeat1(Object[] arr){
Set set = new HashSet();
for (int i = 0; i < arr.length; i++) {
set.add(arr[i]);
}
return set.toArray();
}
}
(第2种方法)
public int removeDuplicates(int[] A) {
if (A == null || A.length == 0) {
return 0;
}
int size = 0;
for (int i = 0; i < A.length; i++) {
if (A[i] != A[size]) {
A[++size] = A[i];
}
}
return size + 1;
}
合并排序数组
合并两个排序的整数数组A和B变成一个新的数组。可以假设A具有足够的空间去添加B中的元素。
public void mergeSortedArray(int[] A, int m, int[] B, int n) {
int i = m - 1, j = n - 1, index = m + n - 1;
while (i >= 0 && j >= 0) {
if (A[i] > B[j]) {
A[index--] = A[i--];
} else {
A[index--] = B[j--];
}
}
while (i >= 0) {
A[index--] = A[i--];
}
while (j >= 0) {
A[index--] = B[j--];
}
}
12、排序 十大经典排序算法详细总结(含JAVA代码实现)
(1)冒泡排序
public static int[] bubbleSort(int[] array) {
if (array.length == 0){
return array;
}
for (int i = 0; i < array.length; i++){
//如果未发生交换,则说明数组已经有序,不必再排序了
boolean isSwap = true;
for (int j = 0; j < array.length - 1 - i; j++){
if (array[j + 1] < array[j]) {
int temp = array[j + 1];
array[j + 1] = array[j];
array[j] = temp;
isSwap = false
}
}
if(isSwap){
break;
}
}
return array;
}
(2)选择排序
public static int[] selectionSort(int[] array) {
if (array.length == 0){
return array;
}
for (int i = 0; i < array.length; i++) {
int minIndex = i;
for (int j = i; j < array.length; j++) {
if (array[j] < array[minIndex]) //找到最小的数
minIndex = j; //将最小数的索引保存
}
int temp = array[minIndex];
array[minIndex] = array[i];
array[i] = temp;
}
return array;
}
(3)插入排序
public static int[] insertionSort(int[] array) {
if (array.length == 0){
return array;
}
int current;
for (int i = 0; i < array.length - 1; i++) {
current = array[i + 1];
int preIndex = i;
while (preIndex >= 0 && current < array[preIndex]) {
array[preIndex + 1] = array[preIndex];
preIndex--;
}
array[preIndex + 1] = current;
}
return array;
}
(4)快速排序
快速排序其实是冒泡排序的升级版,同样的基于两两交换,但是引入了分治的思想。通过使用中线,对需要排序的区间进行了重新的一个划分,而这内部剩下的性能还有一方面就是在于这个中线,因为数值的比较不再是全局,而是局部,从效率计算上来讲一般情况可降到O(nlogn),当然极端情况就可能退化回我们的冒泡排序。
public static void main(String [] args){
int [] arr = {1,4,6,3,7,0,2};
int[] quickSort = QuickSort(arr, 0, arr.length-1);
System.out.println(quickSort);
}
public static int[] QuickSort(int[] array, int start, int end) {
if (array.length < 1 || start < 0 || end >= array.length || start > end) return null;
int smallIndex = partition(array, start, end);
if (smallIndex > start)
QuickSort(array, start, smallIndex - 1);
if (smallIndex < end)
QuickSort(array, smallIndex + 1, end);
return array;
}
public static int partition(int[] array, int start, int end) {
int pivot = (int) (start + Math.random() * (end - start + 1));
int smallIndex = start - 1;
swap(array, pivot, end);
for (int i = start; i <= end; i++)
if (array[i] <= array[end]) {
smallIndex++;
if (i > smallIndex)
swap(array, i, smallIndex);
}
return smallIndex;
}
public static void swap(int[] array, int i, int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
13、数组中重复的数字
在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。
public class Solution {
public boolean duplicate(int numbers[],int length,int [] duplication) {
StringBuffer sb = new StringBuffer();
for(int i=0;i<length;i++){
sb.append(numbers[i]+"");
}
for(int j=0;j<length;j++){
if(sb.indexOf(numbers[j]+"") != sb.lastIndexOf(numbers[j]+"")){
duplication[0] = numbers[j];
return true;
}
}
return false;
}
}
14、把字符串转换成整数
将一个字符串转换成一个整数,要求不能使用字符串转换整数的库函数。 数值为0或者字符串不是一个合法的数值则返回0
public class Solution {
public static int StrToInt(String str) {
if(str.equals("") || str.length() == 0 || str.equals("+") || str.equals("-")){
return 0;
}
char [] newstr = str.toCharArray();
StringBuffer sb = new StringBuffer();
for(int i=0;i<newstr.length;i++){
if(0 < newstr[i] && newstr[i]< 97){
sb.append(newstr[i]);
}else{
return 0;
}
}
return Integer.parseInt(sb.toString().replace("+",""));
}
}
15、跳台阶
一只青蛙一次可以跳上1级台阶,也可跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)
public class Solution {
public int JumpFloor(int target) {
if(target<4){
return target;
}
return JumpFloor(target-1)+JumpFloor(target-2);
}
}
变态跳台阶
一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
public class Solution {
public int JumpFloorII(int target) {
if(target<0){
return target;
}
return (int) Math.pow(2, target - 1);
}
}
16、替换空格
请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。
public class Solution {
public String replaceSpace(StringBuffer str) {
if(str == null){
return str.toString();
}
String newstr = str.toString().replaceAll(" ","%20");
return newstr;
}
}
17、左旋转字符串
汇编语言中有一种移位指令叫做循环左移(ROL),现在有个简单的任务,就是用字符串模拟这个指令的运算结果。对于一个给定的字符序列S,请你把其循环左移K位后的序列输出。例如,字符序列S=”abcXYZdef”,要求输出循环左移3位后的结果,即“XYZdefabc”。是不是很简单?OK,搞定它!
public class Solution {
public String LeftRotateString(String str,int n){
if(str.length()==0 || n<0 ){
return "";
}
return str.substring(n,str.length())+str.substring(0,n);
}
}
18、爬楼梯
假设你正在爬楼梯,需要n步你才能到达顶部。但每次你只能爬一步或者两步,你能有多少种不同的方法爬到楼顶部?
public int climbStairs(int n) {
if (n == 0) return 0;
int[] array = new int[n + 1];
array[0] = 1;
if (array.length > 1) {
array[1] = 1;
}
for(int i = 2; i < array.length; i++) {
array[i] = array[i - 1] + array[i - 2];
}
return array[n];
}