1.认识复杂度和简单排序算法
评估算法优劣的核心指标是什么?
- 时间复杂度(流程决定)
- 额外空间复杂度(流程决定)
- 常数项时间(实现细节决定)
何为常数时间操作?
如果一个操作的执行时间不以具体样本量为转移,每次执行时间都是固定时间。这样的操作为常数时间的操作。
常见的常数时间的操作
- 常见的算数运算(+、-、*、/、%等)
- 常见的位运算(>>、>>>、<<、<<<、|、&、^等)
- 赋值、比较、自增、自减操作等
- 数组寻址操作
如何确定算法流程的总操作数量与样本数量之间的表达式关系?
- 想象该算法流程所处理的数据状况,要按照最差情况来。
- 把整个流程彻底拆分为一个个基本动作,保证每个动作都是常数时间的操作。
- 如果数据量为N,看看基本动作的数量和N是什么关系
时间复杂度
时间复杂度是一个算法流程中,常数操作数量的一个指标。具体来说先对一个算法流程非常熟悉,然后去写出这个算法流程中,发生了多少常数操作,进而总结出常数操作量的表达方式。
如何确定算法流程的时间复杂度?
当完成了表达式的建立,只留下最高阶项留下即可。低价项都去掉,高阶项的常数也去掉。记为:O(忽略掉系数的高阶项)。
选择排序
- 每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到全部待排序的数据元素排完。
- 过程:
- 代码实现
public static void selectSort(int[] arr){
if(arr == null || arr.length < 2){
return;
}
// 0 ~ N-1 找到最小值,在哪,放到0位置上
// 1 ~ n-1 找到最小值,在哪,放到1 位置上
// 2 ~ n-1 找到最小值,在哪,放到2 位置上
int N = arr.length;
for (int i = 0; i < N; i++) {
int min = i;
for (int j = i+1; j < N; j++) {// i ~ N-1 上找最小值的下标
min = arr[j] < arr[min] ? j :min;
}
swap(arr,i,min);
}
}
public static void swap(int[] arr,int i ,int j){
int tmp = arr[j];
arr[j] = arr[i];
arr[i] = tmp;
}
冒泡排序
- 重复地走访过要排序的元素列,依次比较两个相邻的元素,如果顺序错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素列已经排序完成。
- 过程:
- 代码实现:
public static void bubbleSort(int[] arr){
//判断数组是否为空或者数组长度是否只有一个元素
if(arr == null || arr.length < 2){
return;
}
for (int end = arr.length-1; end >0 ; end--) {
for (int i = 0; i < end; i++) {
if(arr[i]>arr[i+1]){
swap(arr,i,i+1);
}
}
}
}
//交换函数swap
public static void swap(int[] arr,int i ,int j){
int tmp = arr[j];
arr[j] = arr[i];
arr[i] = tmp;
}
插入排序
- 对于少量元素的排序,它是一个有效的算法。插入排序是一种最简单的排序方法,它的基本思想是将一个记录插入到已经排好序的有序表中,从而一个新的、记录数增 1 的有序表 。在其实现过程使用双层循环,外层循环对除了第一个元素之外的所有元素,内层循环对当前元素前面有序表进行待插入位置查找,并进行移动。
- 过程:
- 估算时发现这个算法流程的复杂度,会因为数据状况的不同而不同。
- 代码实现:
public static void insertionSort(int[] arr){
if(arr == null || arr.length < 2){
return;
}
for (int i = 1; i <arr.length ; i++) {//0~i做到有序
for (int j = i-1; j >= 0 && arr[j] > arr[j+1] ; j--) {
swap(arr,j,j+1);
}
}
//交换函数swap
public static void swap(int[] arr,int i ,int j){
int tmp = arr[j];
arr[j] = arr[i];
arr[i] = tmp;
}
额外空间复杂度
- 你要实现一个算法流程,在实现算法流程的过程中,你需要开辟一些空间来支持你的算法流程。
- 作为输入参数的空间,不算额外空间。
- 作为输出结果的空间,也不算额外空间。
- 因为这些都是必要的、和现实目标有关的,所以都不算。
- 但除此之外,你的流程如果还需要开辟空间才能让你的流程继续下去,这部分空间就是额外空间。
- 如果你的流程只需要开辟,有限几个变量,额外时间复杂度就是O(1)。
常数项时间
时间复杂度是忽略低价项和所有常数系数的。如果两个时间复杂度一样的算法,还要去在时间上拼优劣,就进入到拼常数时间的阶段,简称拼常数项。
算法流程的常数项的比拼方式
- 放弃理论分析,生成随机数直接测
- 如果纯理论分析,往往会需要非常多的分析过程。都已经到了具体细节的程度,莫不如交给实验数据好了。
2.对数器
- 你想要测得的方法a
- 实现复杂度不好但容易实现的方法b
- 实现一个随机样本产生器
- 把方法a和方法b跑相同的随机样本,看看得到的结果是否一样
- 如果有一个随机样本使得对比结果不一致,打印样本进行人工干预,改对方法a和方法b
- 当样本数量很多时对比测试依然正确,可以确定方法a已经正确。
3.二分法
认识二分法
- 在一个有序数组中,找某个数是否存在
- 在一个有序数组中,找>=某个数最左侧的位置
- 在一个有序数组中,找<=某个数最右侧的位置
- 局部最小值问题
4.异或运算
认识异或运算
- 相同为0,不同为1
- 可以理解为:异或运算就记成
无进位相加
异或运算的性质
- 0^N==N,N^N==0
- 异或运算满足交换律和结合律
题目一:如何不用额外变量交换两个数
- 思路:利用N^N==0,a=a^b^a,b=a^b^b
- 注意:前提是a和b的内存地址不同,地址相同时会被洗成0
- 代码实现:
public static void swap(int a,int b){
a=a^b;
b=a^b;//b=a^b^b-->b=a^0-->b=a
a=a^b;//a=a^b^a-->a=b^0-->a=b
}
题目二:一个数组中有一种数出现了奇数次,其他都出现了偶数次,怎么找到并打印这种数。
- 思路:int eor=0;把eor从数组头异或到数组尾,结束时eor就是出现了奇数次的数
- 代码实现:
public static void OddTimesNum(int arr[],int len){
int eor = 0;
for (int i = 0; i < len; i++) {
eor ^= arr[i];
}
System.out.println("数组中次数为奇数的数为: " + eor);
}
题目三:怎么把一个int类型的数,提取出最右侧的1来。
- 思路:N & (~N +1)
- 代码实现:
int rightOne = eor & (~eor + 1);//提取出最右边的1
题目四:一个数组中有两种数出现了奇数次,其他都出现了偶数次,怎么找到并打印这两种数。
- 思路:int eor =0;设出现奇数次数为a和b,把eor从数组头异或到数组尾,结束时eor就是a^b
- 所以eor=a^b (a!=b ——》eor!=0)
- eor一定在某一位(至少一位)不等于0 ,假设第X位为1,说明a和b在第X位不一样
- int eor'=0;把eor’从数组头异或到数组尾,只异或数组中那些X位不为0的数,结束时eor'就是a或者b
- X位0的数不影响结果,只异或数组中那些X位不为0的数时,other2中1的个数为偶数次全消除,只剩a或b中一个, eor'只能碰到a或者b中一个,得到的结果就是另外一个eor^eor' 是a或b的另外一个
- 代码实现:
public static void printOddTimesNum2(int[] arr) {
int eor = 0;
for (int i=0;i<arr.length;i++) {
eor^= arr[i];
}
//eor=a^b
//eor!=0
//eor必然有一个位置是1
int rightOne = eor & (~eor + 1);//提取出最右边的1
int eorhasone=0;//eor'
for (int cur : arr) {
if ((cur & rightOne) != 0) {
eorhasone ^= cur;
}
}
System.out.println(eorhasone + " " + (eor ^ eorhasone));
}