1. 排序算法
主流的排序算法,按照时间复杂度分为三大类:
1)时间复杂度为 O(n2)
- 冒泡排序
- 选择排序
- 插入排序
2)时间复杂度为O(nlogn)
- 快速排序
- 堆排序
- 归并排序
3) 时间复杂度为线性的排序算法
- 计数排序
- 桶排序
- 计数排序
我们这里挑几种 来说 :
冒泡排序
顾名思义,向小水泡一样,咕噜咕噜的往前走,一圈遍历,得到一个最大的值;再一圈得到n-1 打的值;
以下面的数组为例,我们分析 一次遍历的数组变化;

因为排序过程无新增内存空间,空间复杂度:O(1);
因为每一次都是与所有元素(大约)比较,时间复杂度:O(n2);
示例代码如下:
// 从小到大排序public void bubbleSort(int [] a){for(int i = a.length - 1 ; i > 0 ; i--){boolean isChange = false;int ci = i;for(int j = i - 1 ; j >= 0 ; j--){if(a[j] > a[ci]){ // 大于,交换int temp = a[ci];a[ci] = a[j];a[j] = temp ;ci = j;isChange = true;}}if(!isChange){ // 未发生变化,说明顺序已OK,返回return;}}}
堆排序
堆排序需要对二叉堆有所了解,堆排序用的是最大堆和最小堆;
最大堆定义:父节点大于任何一个子节点的值;
堆在数组中的表示行为如下:
假设父节点为 p,
左节点: l = 2 p + 1;
右节点: r = 2 p + 2;
以从小到大堆排序的为例,说一下堆排序步骤:
1)将目标数组转化为最大堆;
2)a[0] 与 a[n-1] 交换;
3)将0~n-2 再次排成最大堆;
4)a[0] 与 a[n-2] 交换;
5)重复3、4步骤;
示例代码如下:
// 向下调整public void down(int [] a ,int start ,int end){int p = start;int l = 2 * p + 1;while(l < end){if(a[l] < a[l+1]){ // 取最大的子节点l++;}if(a[p] < a[l]){ // 最大孩子 > 父节点,交换int temp = a[p];a[p] = a[l];a[l] = temp;}p = l;l = 2 * l + 1;}}// 堆排序public void heapSort(int [] a){// 将数组转化为最大堆for(int i = a.length/2 - 1 ; i >= 0 ; i--){down(a, i , a.length - 1);}// 一次取最大元素,完成排序for(int i = a.length - 1; i > 0 ; i--){int temp = a[0];a[0] = a[i];a[i] = temp;// 再次转化为最大堆down(a,0,i-1);}}
快速排序
快速排序,思想有点类似于2分法;
步骤:
- 1)定义两个指针,一个left, 一个right; 定义一个基准点,通常去第一个元素;
- 2)left 向右移动 找大于基准点的值,right 向左自动,找小于基准点的值;
- 3)同时找到,或者 left > right ,停止;此时 left 或 right 左边是小于的,右边是大于的;
- 4)分隔数组,0~left, left ~n-1;
- 5)重复执行以上步骤,完成排序;
依次轮询示例图:
示例代码:
// 快速排序public void quickSort(int [] a ,int left ,int right){int point = left;int start = left;int end = right;if(left < right){while(left < right){while(left < right && a[left] < a[point]){left++;}while(left < right && a[right] > a[point]){right--;}int temp = a[left];a[left] = a[right];a[right] = temp;}quickSort(a,start, point - 1);quickSort(a,point + 1, end);}}
2. 经典算法
这部分内容为常见的面试算法:
2.1 如何判断链表有环?
示例环如下:
方式一:遍历链表,将链表存入Set集合,若新入元素在Set集合中已存在,则此链表有环;
这种方式:时间复杂度O(n),空间复杂度也是O(n);
方式二:定义两个指针,同时向右移动,一个步长为1,一个步长为2,两个指针若相遇,则此链表中有环;
示例代码:
链表节点定义:
@Datapublic class LinkNode{private Integer data;private LinkNode next;}
public class RingLink{// 获取一个环形链表public LinkNode initData(){LinkNode n1 = new LinkNode();LinkNode n2 = new LinkNode(2,n6);LinkNode n6 = new LinkNode(6,n8);LinkNode n8 = new LinkNode(8,n1);LinkNode n7 = new LinkNode(7,n2);LinkNode n3 = new LinkNode(3,n3);LinkNode n5 = new LinkNode(5,n3);n1.setData(1);n1.setNext(n2);}// 获取相遇的点public LinkNode getBingPoint(LinkNode link){LinkNode v1 = link.next;LinkNode v2 = link.next.next;while(!Objects.equals(v1,v2)){v1 = v1.next;LinkNode v2 = v2.next.next;}return v1;}// 求出环的起始点public LinkNode getRingStart(LinkNode bingNode,LinkNode link){LinkNode v1 = bingNode.next;LinkNode v2 = link.next;while(!Objects.equals(v1,v2)){v1 = v1.next;v2 = v2.next;}return v1;}}
2.2 最小栈
实现一个栈,该栈带有出栈(pop)、入栈(push)、取最小元素(get_min)3个方法。要保证这3个方法的时间复杂度都是O(1) 最小栈实现;
解法思想:题目说了时间复杂度O(1), 那么肯定要牺牲空间了,
- 我们准备一个与目标栈A一样的栈B;
- push() 时 ,如果新入元素比 栈顶元素小,将此元素也入栈B;
再次入栈时,先与栈B比较,如果 < 栈B的栈顶元素,将此元素也入栈 B;
- pop() 时,如果当前 栈B 的栈顶元素,与 栈 A的栈顶元素一样,那么同时pop();
示例图:
示例代码:
/**** 实现一个栈,该栈带有出栈(pop)、入栈(push)、取最小元素(get_min)3个方法。要保证这3个方法的时间复杂度都是O(1)* 最小栈实现*/public class MinStack {private int [] stackA = new int[1024];private int [] stackB = new int[1024];private int currentA = 0;private int currentB = 0;// 入栈public void push(int value){stackA[currentA] = value;if(currentA == 0 || value < stackB[currentB - 1]){stackB[currentB] = value;currentB++;}currentA++;}// 出站public int pop(){int value = stackA[currentA - 1];if(value == stackB[currentB - 1]){stackB[currentB - 1] = 0;currentB--;}stackA[currentA - 1] = 0;currentA--;return value;}// 获取最小值public int getMin(){if(currentB <= 0){return -1;}return stackB[currentB - 1];}public static void main(String[] args) {MinStack stack = new MinStack();stack.push(4);stack.push(9);stack.push(7);stack.push(3);stack.push(8);stack.push(5);System.out.println("stack pop " + stack.pop() + " , min = " + stack.getMin());System.out.println("stack pop " + stack.pop() + " , min = " + stack.getMin());System.out.println("stack pop " + stack.pop() + " , min = " + stack.getMin());System.out.println("stack pop " + stack.pop() + " , min = " + stack.getMin());System.out.println("stack pop " + stack.pop() + " , min = " + stack.getMin());System.out.println("stack pop " + stack.pop() + " , min = " + stack.getMin());}}
2.3 如何求出最大公约数
辗转相除法
两个正整数a,b(a>b), 它们的最大公约数 = a除以b的余数c和较小数b之间的最大公约数。
例如:10和25, 25除以10=2…5, 那么10和25的最大公约数, 等同于10和余数5的最大公约数。
更相减损术
两个正整数a,b(a>b), 它们的最大公约数 = a-b的差值c和较小数b之间的最大公约数。
例如:10和25, 25-10=15, 那么10和25的最大公约数, 等同于10和15的最大公约数。
两个整数较大时候的场景,取模性能会比较差,更相减损术更适合。但如果两个数相差悬殊,比如10000和1时,更相减损术就要递归9999次。此时可以用方法三
更相减损术+移位(最优):
- 当a,b均为偶数时,gcd(a,b) = 2gcd(a/2,b/2) = 2gcd(a>>1,b>>1),gcd为方法名;
- 当a为偶数,b为奇数时,gcd(a,b) = gcd(a/2,b) = gcd(a>>1,b);
- 当a为奇数,b为偶数时,gcd(a,b) = gcd(a,b/2) = gcd(a,b>>1);
- 当a,b均为奇数,先使用更相减损术运算一次,gcd(a,b) = gcd(b,a-b),此时a-b必然是偶数,然后又可以继续进行移位运算
ps. >>相当于除以2,<<相当于乘以2
/*** 更相减损数 + 辗转相除 优化版*/public Integer gcd(int a, int b) {if (a == b) {return a;}if ((a & 1) == 0 && (b & 1) == 0) { // a b 均为偶数return gcd(a >> 1, b >> 1) * 2;} else if ((a & 1) != 0 && (b & 1) == 0) { // a b 均为偶数return gcd(a, b >> 1);} else if ((a&1) == 0 && (b&1)!=0 ){ // a b 均为偶数return gcd(a >> 1, b);}else{int max = Math.max(a, b);int min = Math.min(a, b);return gcd(max - min, min);}}
2.4 给定一个数组,求相邻元素的最大差值
/*** 给定一个数组,求相邻元素的最大差值*/public class MaxArrayGap {public int getMaxGap(int [] array){int [] minArray = new int[array.length + 1];int [] maxArray = new int[array.length + 1];boolean [] hasArray = new boolean[array.length + 1];int max = array[0];int min = array[0];// 获取最大值 和最小值for (int loop : array) {max = Math.max(max, loop);min = Math.min(min,loop);}// 将数据插入到新定义的桶中for (int loop : array) {// 计算当前值 在桶中的位置 max / array.length = loop/positionint position = (loop * array.length) / max;minArray[position] = loop;maxArray[position] = loop;if(hasArray[position]){minArray[position] = Math.min(minArray[position],loop);maxArray[position] = Math.max(maxArray[position],loop);}hasArray[position] = true;}// 遍历桶 ,取间隔最大的数据int maxGap = 0;int previousValue = maxArray[0];for (int i = 1; i <= array.length; i++) {if(hasArray[i]){maxGap = Math.max(minArray[i] - previousValue,maxGap);previousValue = maxArray[i];}}return maxGap;}@Testpublic void test() {int[] array = {0, 6, 3, 16, 7, 10, 9, 11, 20, 18};int maxGap = getMaxGap(array);System.out.println("最大的间隔:" + maxGap);}}
2.5 用栈来模拟一个队列,要求实现队列的两个基本操作:入队、出队
思想:使用两个栈,一个用于出,一个用于进。
package com.uu.study;import org.junit.Test;import java.util.Stack;/*** 用栈来模拟一个队列,要求实现队列的两个基本操作:入队、出队*/public class StackQueue {Stack<Integer> a = new Stack<>();Stack<Integer> b = new Stack<>();public void push(Integer value){a.push(value);}public Integer pop(){if(b.isEmpty()){ // b = null ,先填充Bif(a.isEmpty()){return null;}Integer pop = null;while (!a.isEmpty()){b.push(a.pop());}}return b.pop();}@Testpublic void test(){StackQueue queue = new StackQueue();for (int i = 1; i <= 9; i++) {queue.push(i);}for (int i = 1; i <= 9; i++) {System.out.println(queue.pop());}}}
2.6 获取全排列的下一个数
示例: 1,2,4,7,6,3
下一个数: 1,2,6,3,4,7
思想:
1)反向获取数组,获取第一个逆序位置;
2)获取逆序位置 到 最后一个数 中 刚刚大于 它的数字进行交换 ;
3)将 第一个逆序位置 到 最后一个 顺序反转;
注:逆序位置 至 最后一位,是有序的(这点要清楚哈😄,理解的关键);
public class FullNextNum{public void getFullNextNum(int [] array){// 1) 获取第一个逆序 位置 数int unOrderIndex = 0;for(int i = array.length - 1 ; i > 0 ; i--){if(array[i - 1] < array[i]){unOrderIndex = i;break;}}// 2) 逆序数-1 与 刚刚大于这个数的位置交换int temp = array[unOrderIndex - 1];for(int i = array.length - 1 ; i > unOrderIndex; i-- ){if(temp < array[i]){array[unOrderIndex - 1] = array[i];array[i] = temp;break;}}// 3) 将 unOrderIndex 后的数据 反转for(int i = unOrderIndex,j = array.length - 1; i < j ; i++,j--){int t = array[i];array[i] = array[j];array[j] = t;}}}

