一、概述
算法是进入大厂的敲门砖,也是每位程序员必备技能,从今天开始,博主将仔细和大家聊聊算法和数据结构,努力记下这些,在面试中立于不败之地!
二、分析
1.认识时间复杂度
常数操作:一个操作如果与样本的数据量没有关系,每次都是固定时间内完成的操作,称为常数操作。
时间复杂度为一个算法流程中,常数操作数量的一个指标。常用0(读作big o)来表示。具体来说,先要对一个算法流程非常熟悉,然后去写出这个算法流程中,发生了多少常数操作,进而总结出常数操作数量的表达式。在表达式中,只要高阶项,不要低阶项,也不要高阶项的系数,如果剩下的部分为f(N),那么时间复杂度为O(f(N))。
评价一个算法流程的好坏,先看时间复杂度的指标,然后再分析不同数据样本下时间运行的时间,也就是“常数项时间”。
2.选择排序——细节与复杂度分析
代码如下:
/**
* 选择排序
* @param arr 目标数组
*/
public static void selectionSort(int[] arr){
if(arr==null||arr.length<2){ //如果条件是arr.length<1,则无法产生双重for循环
return ;
}
for (int i = 0; i < arr.length-1; i++) { //不写i < arr.length是因为j = i+1会越界
int index = i;
//获得i循环内元素值最小的下标
for (int j = i+1; j < arr.length; j++) {
//这段代码可以被优化
/*if(arr[index]>arr[j]){
index = j;
}*/
//巧用三元运算减少代码行
index = arr[index]>arr[j]?j:index;
}
if(i!=index){
//交换数组arr下标为a和下标为b的值
swap(arr,i,index);
}
}
}
/**
* 交换数组arr下标为a和下标为b的值
* @param arr
* @param a
* @param b
*/
public static void swap(int[] arr,int a,int b){
arr[a] = arr[a] ^ arr[b];
arr[b] = arr[a] ^ arr[b];
arr[a] = arr[a] ^ arr[b];
}
复杂度分析:因为是双重for循环,那么时间复杂度自然是O(N^2),额外空间复杂度为O(1)。
3.冒泡排序——细节与复杂度分析
代码如下:
/**
* 冒泡排序
* @param arr 目标数组
*/
public static void selectionSort(int[] arr){
if(arr==null||arr.length<2){ //如果条件是arr.length<1,则无法产生双重for循环
return ;
}
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr.length-1; j++) {
//每次比较,将较大的数往后移
if(arr[j]>arr[j+1]){
swap(arr,j,j+1);
}
}
}
}
/**
* 交换数组arr下标为a和下标为b的值
* @param arr
* @param a
* @param b
*/
public static void swap(int[] arr,int a,int b){
arr[a] = arr[a] ^ arr[b];
arr[b] = arr[a] ^ arr[b];
arr[a] = arr[a] ^ arr[b];
}
复杂度分析:因为也是双重for循环,那么和选择排序一样,时间复杂度自然是O(N^2),额外空间复杂度为O(1)。
3.补充:异或运算如何交换两个int变量的值?
基本:二进制位上,同一位上相同,则取0,不同则取1。
举例:0^1=1 ,1^0=1,0^0=0,1^1=1
理解:无进位相加
性质:
(1)0^N=N,N^N=0
(2)满足交换律和结合律:a^b = b^a,(a^b)^c=a^(b^c)
(3)当一组数异或一个数N,得到的结果是不随着这组数中与N异或顺序的改变而改变的。
由上诉性质,我们就能明白为什么不需要额外变量就可以交换a和b的值,具体步骤如下:
注意:当a和b值相同,却不指向同一内存空间时,可以采用此方法。但是a和b指向同一块内存空间的时候,不能采用此方法,因为等同于同一个内存空间异或,最终值会变成0。
拓展练习题:
1、在一个int数组arr中,只有一种数字出现了奇数次,其他数字出现了偶数次,如何找到这种数?
解答:将arr中所有数进行异或运算,得出的结构,就是奇数次的数。
2、若在题1中,有两种数字出现了奇数次,其他数字出现了偶数次,如何找到这两种数?
解答:
(1)假设出现了奇数次的数是a和b;
(2)设eor为arr中所有数进行异或运算得出的结果,则eor = a ^ b;
(3)又因为a不等于b,且a,b都不等于0,所以eor化为二进制后,至少有一位不为0,假设次位是k位;
(4)将数组arr分为两类,一类是k位为1的数组成arr1,一类是k位为0的数arr0;
(5)将k位是1,其他位是0的数字与arr1或arr0进行异或得到eor2,eor2便是a或者b;
(6)再将eor2和eor进行异或,便可以得到另一个数。
取出eor为1的最低位的数字原理如下:
4.插入排序——细节与复杂度分析
代码如下:
/**
* 插入排序
* @param arr 目标数组
*/
public static void insertionSort(int[] arr){
if(arr==null||arr.length<2){ //如果条件是arr.length<1,则无法产生双重for循环
return ;
}
for (int i = 1; i < arr.length; i++) {
for (int j = i-1; j>=0&&arr[j]>arr[j+1]; j--) { //不能写作arr[j]>arr[i],会导致后续每个位置都是和i位置比较
//j+1位置上的数如果小于j位置上的数,则交换
swap(arr,j+1,j);
}
}
}
/**
* 交换数组arr下标为a和下标为b的值
* @param arr
* @param a
* @param b
*/
public static void swap(int[] arr,int a,int b){
arr[a] = arr[a] ^ arr[b];
arr[b] = arr[a] ^ arr[b];
arr[a] = arr[a] ^ arr[b];
}
注意:插入排序比较特殊,时间复杂度会因为数据本身的顺序所影响。例如int a[] = {6,5,4,3,2,1},时间复杂度是O(N^2);而int a[] = {1,2,3,4,5,6},则时间复杂度是O(N)。额外的空间复杂度都是O(1)。但是算法流程一般都是按照最差的情况来估算时间复杂度,所以插入排序的时间复杂度也是O(N^2)。
5.二分法的详解与扩展
(1)在一个有序数组arr[]中,找某个数num是否存在
解析:arr[]先取中点k的数,如果arr[k]>num,则表示可以抛弃arr[k]右边的元素;继续在arr[0]到arr[k-1]上进行上述操作,直至找到等于num的位置。时间复杂度为O(logN),如下图:
(2)在一个有序数组arr[]中,找>=某个数num最左侧的位置(<=某个数同理)
解析:arr[]先取中点k的数,如果arr[k]>=num,则用t记录下此时k的位置;继续在arr[0]到arr[k-1]上进行上述操作,若中点的数还是满足>=num,则更新t为此时的位置,继续进行上述操作,直至不能继续二分;若在arr[0]到arr[k-1]上进行上述操作,中点的数不满足>=num,则不更新t的值,而在右侧继续进行上述操作,直至不能继续二分。时间复杂度为O(logN),如下图:
(3)局部最小值(在0到1位置上,若arr[0]<arr[1],则arr[0]是局部最小;在n到n-1位置上,若arr[n]<arr[n-1],则arr[n]是局部最小;在i-1到i+1位置上,若arr[i]<arr[i-1]&&arr[i]<arr[i+1],则arr[i]是局部最小)问题:在一个长度为N-1数组中arr[]无序,但任意两个相邻的数不相等,求一个局部最小的位置。
解析:先判断0位置和N-1位置是不是局部最小,若不是则取中点位置M,判断在M-1到i+M位置上,M是否局部最小;若不是,则看趋势是往左递减还是往右递减,往哪边递减,就在哪边再取中间位置重复上述操作即可。时间复杂度为O(logN),如下图:
注意:不是所有的二分都是在有序数组上进行的。
流程优化方向:
(1)数据状况。数据状况特殊,可以被优化。
(2)求的问题的标准。问的问题比较特殊,可以被优化。
6.对数器的概念和使用
概念:
(1)有一个你想要测的方法a;
(2)实现复杂度不好,但是容易实现的方法b;
(3)实现一个随机的样本产生器
(4)把方法a和方法b跑相同的随机样本,看得到的结果是否一样
(5)如果有一个随机样本使得比对结果不一致,打印样本进行人工干预,改对方法a或者b
(6)当样本数量很多是比对测试依然正确,则可以确定方法a已经正确
使用:
public class test2 {
/**
* 测试冒泡
* @param args
*/
public static void main(String[] args) {
int testTime = 500;
int maxSize = 100;
int maxValue = 100;
boolean succeed = true;
for (int i = 0; i < testTime; i++) {
int[] arr1 = generateRandomArray(maxSize,maxValue);
int[] arr2 = copyArray(arr1);
selectionSort(arr1);
comparator(arr2);
if(!isEqual(arr1,arr2)){
succeed = false;
break;
}
}
System.out.println(succeed?"Nice!":"Bad!");
}
/**
* 冒泡排序
* @param arr 目标数组
*/
public static void selectionSort(int[] arr){
if(arr==null||arr.length<2){ //如果条件是arr.length<1,则无法产生双重for循环
return ;
}
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr.length-1; j++) {
//每次比较,将较大的数往后移
if(arr[j]>arr[j+1]){
swap(arr,j,j+1);
}
}
}
}
/**
* 交换数组arr下标为a和下标为b的值
* @param arr
* @param a
* @param b
*/
public static void swap(int[] arr,int a,int b){
arr[a] = arr[a] ^ arr[b];
arr[b] = arr[a] ^ arr[b];
arr[a] = arr[a] ^ arr[b];
}
public static int[] generateRandomArray(int maxSize,int maxValue){
//Math.random() -> [0,1) 所有的小数,等概率返回一个
//Math.random()*N -> [0,N) 所有的小数,等概率返回一个
//(int)Math.random()*N -> [0,N-1] 所有的整数,等概率返回一个
int[] arr = new int[(int)((maxSize+1)*Math.random())]; //长度随机
for (int i = 0; i < arr.length; i++) {
// arr[i] = (int)((maxSize+1)*Math.random());
arr[i] = (int)((maxSize+1)*Math.random())-(int)((maxSize)*Math.random()); //可以得到负数,增加随机性
}
return arr;
}
/**
* 判断两个数组元素顺序是否完全一致
* @param arr1
* @param arr2
* @return
*/
public static boolean isEqual(int[] arr1, int[] arr2){
if((arr1==null&&arr2!=null)||(arr1!=null&&arr2==null)){
return false;
}
if(arr1==null&&arr2==null){
return true;
}
if(arr1.length!=arr2.length){
return false;
}
for (int i = 0; i < arr1.length; i++) {
if(arr1[i] != arr2[i]){
return false;
}
}
return true;
}
/**
* 复制一个数组arr
* @param arr
* @return
*/
public static int[] copyArray(int[] arr){
if(arr==null){
return null;
}
int[] res = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
res[i] = arr[i];
}
return res;
}
/**
* 默认排序
* @param arr
*/
public static void comparator(int[] arr){
Arrays.sort(arr);
}
}
三、总结
本文介绍了选择排序、冒泡排序、插入排序等基础排序,主要是希望根据简单的基础排序带领大家认识时间复杂度的概念。下一篇博主将介绍几个重要的排序算法如归并排序、堆排序等详解,敬请期待《算法从入门到精通(二):认识O(NlogN)的排序》。
更多精彩内容,敬请扫描下方二维码,关注我的微信公众号【Java觉浅】,获取第一时间更新哦!