2018年10月8日
/*
本节主要内容:
1、 时间复杂度
2、冒泡排序
3、选择排序
4、插入排序
5、对数器概念和使用
6、递归行为的时间复杂度
7、归并排序
8、快速排序
9、堆排序
10、排序算法的总结和排序稳定性总结
11、工程中的综合排序算法
12、比较器的作用
*/
时间复杂度:
常数时间的操作:
-------------------------------------------------------------------------
一个操作如果和数据量没有关系,每次都是固定时间内完成的操作,叫做常数操作。
时间复杂度的表示:
-------------------------------------------------------------------------
时间复杂度为一个算法流程中,常数操作数量的指标。常用O (读作big O)来表示。
具体来说,在常数操作数量的表达式中
只要高阶项,不要低阶项,也不要高阶项的系数,剩下的部分 如果记为f(N),那么时间复杂度为O(f(N))。
评价一个算法流程的好坏
-------------------------------------------------------------------------
评价一个算法流程的好坏,先看时间复杂度的指标,然后再分析不同数据样本下的实际运行时间,也就是常数项时间
理解额外空间复杂度:
要排序的数组不算做额外的空间,因为我是在提供的数组上进行操作的,就不算作额外的空间
但是像归并排序:需要借助额外的数组,因此归并排序的额外空间复杂度为O(N),选择排序,冒泡排序,插入排序的额外空间复杂度为O(1)
二分查找
在排好序的数组中查询一个数,时间复杂度为O(log2(N)),也可以写作O(logN)
时间复杂度例子:
一个有序数组A,另一个无序数组B,请打印B中的所有不在A中的数,A数组长度为N,B数组长度为M。
算法流程1:对于数组B中的每一个数,都在A中通过遍历的方式找一下;
--------------------------------
时间复杂度为:O(M * N)
算法流程2:对于数组B中的每一个数,都在A中通过二分的方式找一下;
--------------------------------
时间复杂度为:O(M * logN)
因为二分查找的时间复杂度为O(logN)
算法流程3:先把数组B排序,然后用类似外排的方式打印所有不在A中出现的数;
-------------------------------
时间复杂度分析:
①B数组进行排序,假设时间复杂度为O(M * logM)
②类似外排的方式检查,时间复杂度为:O(M + N)
指针a最多滑动N次,指针b最多滑动M次,且每次只滑动a和b中的一个,最差是a和b都走完了这两个数组
即时间复杂度为:O(M + N)
③故而时间复杂度为:O(M * logM) + O(M + N)
如何分析好坏?
如果A数组长,B数组短,则算法3更好,因为此时N更大
如果A数组短,B数组长,则算法2更好,因为此时N小,M大
冒泡排序:
public static void sort(Comparable[] a){
if(a == null || a.length < 2){
return;
}
for(int end = a.length - 1;end > 0;end--){
for(int i = 0;i < end;i++){
if(less(a,i+1,i))
exch(a,i,i+1);
}
}
}
冒泡排序时间复杂度:
N + (N-1) + (N-2)+ ...... + 1 = O(N2)
冒泡排序额外空间复杂度O(1)
选择排序
public static void sort(Comparable[] a){
if(a == null || a.length < 2){
return;
}
for(int i = 0;i < a.length;i++){
int min = i;
for(int j = i+1;j < a.length;j++){
if(less(a,j,min))
min = j;
}
exch(a,i,min);
}
}
选择排序时间复杂度:
N + (N-1) + (N-2)+ ...... + 1 = O(N2)
选择排序额外空间复杂度O(1)
插入排序
-
首先不同于前面冒泡和选择排序的是:冒泡和选择的时间复杂度是与数据状况无关的
冒泡:总是两两比较,最大的交换到右边
选择:每次都选出最小的,交换到最前面
这两个都和数据状况无关,时间复杂度都是O(N2)
- 插入排序则不同:
①当数据有序时:1,2,3,4,5
此时时间复杂度为0(N),因为此时对于每一个i,不需要j的移动,因此为O(N)
②当数据完全倒序时:5,4,3,2,1
此时时间复杂度为O(N2),因为此时对于从第二数开始的每一个数,都需要走到最开始才能回到正确位置,
因此要移动1 + 2 + 3 + 4 + 5 + ...... N 也就是O(N2)
③因此插入排序的时间复杂度为:O(N2)
- 当数据状况不同产生的算法流程不同时,一律按照最差的来算
因此插入排序的时间复杂度为:O(N2)
插入排序额外空间复杂度O(1)
对数器:
验证算法正确性:小样本验证大样本
贪心策略:验证贪心策略的正确性
对数器的概念和使用
0,有一个你想要测的方法a,
1,实现一个绝对正确但是复杂度不好的方法b
2,实现一个随机样本产生器
3,实现比对的方法
4,把方法a和方法b比对很多次来验证方法a是否正确。
5,如果有一个样本使得比对出错,打印样本分析是哪个方法出错
6,当样本数量很多时比对测试依然正确,可以确定方法a已经正确。
代码实现:
/**
* 0,有一个你想要测的方法a——此时为插入排序的代码
*/
public static void sort(int[] a){
int N = a.length;
for(int i = 1;i < N;i++){
int tmp = a[i];
int j = i;
while(j - 1 >= 0 && less(tmp,a[j-1])){
a[j] = a[j -1];
j--;
}
a[j] = tmp;
}
}
private static boolean less(Comparable c0, Comparable c1) {
return c0.compareTo(c1) < 0;
}
/**
* 1,实现一个绝对正确但是复杂度不好的方法b
*/
public static void rightMethod(int[] a){
Arrays.sort(a);
}
/**
* 2,实现一个随机样本产生器
*/
public static int[] generateRandomArray(int maxSize,int maxValue){
//此时该数组中:长度随机,每一个位置上的元素随机
//生成长度随机的数组
int[] a = new int[(int) ((maxSize + 1) * Math.random())]; //此时数组的长度为[0,maxSize]整数
for (int i = 0; i < a.length; i++) {
a[i] = (int) ((maxValue + 1) * Math.random()) - (int) ((maxValue) * Math.random()); //产生正数或者负数
}
return a;
}
// 3,实现比对的方法
private static boolean isEqual(int[] a1, int[] a2) {
if((a1 == null && a2 != null) || (a1 != null && a2 == null))
return false;
if(a1 == null && a2 == null)
return true;
if(a1.length != a2.length)
return false;
for(int i = 0;i < a1.length;i++) {
if(a1[i] != a2[i])
return false;
}
return true;
}
/**
*
* 4,把方法a和方法b比对很多次来验证方法a是否正确。
* for test
*/
public static void main(String[] args) {
int testTime = 500000;
int maxSize = 10; //产生数组长度为10的随机数组
int maxValue = 100;
boolean succeed = true;
for (int i = 0; i < testTime; i++) {
int[] a1 = generateRandomArray(maxSize, maxValue);
int[] a2 = copyArray(a1);
int[] a3 = copyArray(a1);
sort(a1);
rightMethod(a2);
if(!isEqual(a1,a2)){
succeed = false;
//打印出错的数组,printArray是自己实现的打印的方法
printArray(a3);
break;
}
}
System.out.println(succeed ? "Nice":"F@#K");
int[] arr = generateRandomArray(maxSize,maxValue);
printArray(arr);
sort(arr);
printArray(arr);
}
/**
* 数组打印
*/
private static void printArray(int[] a) {
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
System.out.println();
}
/**
* 数组的拷贝
*/
private static int[] copyArray(int[] a) {
int[] aCopy = new int[a.length];
for (int i = 0; i < a.length; i++) {
aCopy[i] = a[i];
}
return aCopy;
}
------------------------------------------------------------------------------------
面试时:准备对数器:排序,堆,二叉树
2018年10月9日
剖析递归行为和递归行为时间复杂度的估算
举个例子
/*
在数组中找最大值——递归来实现
*/
public static int getMax(int[] a,int lo,int hi) {
if(lo == hi)
return a[lo];
int mid = (lo + hi) /2;
int getMaxLeft = getMax(a,lo,mid);
int getMaxRight = getMax(a, mid + 1, hi);
return Math.max(getMaxLeft, getMaxRight);
}
递归函数就是系统在帮你压栈
分析递归函数行为的通式:
master公式:
T(N) = a * T(N/b) + O(N^d)
master公式适用于每一次划分的子问题的规模一致的情况下
其中参数的含义:
N/b:子过程的样本量
a : 子过程量发生的次数
O(N ^ d) :除去调用子过程外,剩下的过程的时间复杂度
T(N):样本量为N时的时间复杂度
如上算法:
样本量为N/2的过程发生了两次,为2 * T(N/2)
子过程发生的次数为2
除去调用子过程,我们剩下的过程的时间复杂度为:O(1)——因为只进行了一个两个数之间求最大值的运算
因此:
此时的a = 2,b = 2,d = 0
则时间复杂度的分析为:
log(b,a) > d ==> 复杂度为O(N ^ log(b,a))
log(b,a) = d ==> 复杂度为O(N^d * logN)
log(b,a) < d ==> 复杂度为O(N^d)
此时我们的算法满足的是:log(b,a) > d,则时间复杂度为:O(N)
归并排序:
public class Merge {
private static Comparable[] aux;
public static void sort(Comparable[] a){
if(a == null|| a.length < 2)
return;
aux = new Comparable[a.length];
sort(a,0,a.length - 1);
}
public static void sort(Comparable[] a,int lo,int hi){
if(hi <= lo){
return;
}
int mid = lo + (hi - lo)/2;
sort(a,lo,mid);
sort(a,mid + 1,hi);
merge(a,lo,mid,hi);
}
private static void merge(Comparable[] a, int lo, int mid, int hi) {
int i = lo;
int j = mid + 1;
for (int k = lo; k <= hi; k++) {
aux[k] = a[k];
}
for (int k = lo; k <= hi; k++) {
if(i > mid) a[k] = aux[j++];
else if (j > hi) a[k] = aux[i++];
else if(less(aux[i],aux[j])) a[k] = aux[i++];
else a[k] = aux[j++];
}
}
private static boolean less(Comparable c0, Comparable c1) {
return c0.compareTo(c1) < 0;
}
}
时间复杂度分析:
①子过程样本量为N/2——>也就是b = 2
②子过程发生次数为2——>也就是a = 2
③除去调用子过程外,剩下的过程为两个有序数组的合并,时间复杂度为:O(N/2 + N/2) = O(N) ——>d = 1
④满足 log(b,a) = d ==> 复杂度为O(N^d * logN)
则时间复杂度:O(N * logN),额外空间复杂度为:O(N)
快速排序:(此时为随机快排,最常用的排序算法)
public static void sort(Comparable[] a){
if(a == null || a.length < 2)
return;
StdRandom.shuffle(a);
sort(a, 0, a.length - 1);
}
private static void sort(Comparable[] a, int lo, int hi) {
if (hi <= lo) {
return;
}
int pos = partition(a,lo,hi);
sort(a,lo,pos-1);
sort(a, pos + 1, hi);
}
/关键代码/
private static int partition(Comparable[] a, int lo, int hi) {
Comparable v = a[lo];
int i = lo;
int j = hi + 1;
while (true) {
while(less(a[++i],v)) if(i == hi) break;
while(less(v,a[--j])) if(j == lo) break;
if(j <= i)
break;
exch(a, i, j);
}
//交换a[lo]和a[j]
exch(a,lo,j);
return j;
}
private static void exch(Comparable[] a, int i, int j) {
Comparable tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}
private static boolean less(Comparable c0, Comparable c1) {
return c0.compareTo(c1) < 0;
}
时间复杂度分析:
关键代码在于:partition
每一次partition均会固定第一个为partition item
-----------------------------------------------
此时的快速排序的算法的时间复杂度是与数据状况有关系的
例如:
情况一:最坏情况1
[0,1,2,3,4,5,6]
partition(a,0,6)
----------------------------------------------------------------------------
当进入partition时,由于i所指向的1大于0,因此i停止移动,此时i == 0
j从6出发,一直大于0,知道j == 0停止
此时j <= i退出循环
最后交换a[0]和a[0]
一共比较N-1次,交换一次
时间复杂度为O(N)
----------------------------------------------------------------------------
partition(a,1,6)
----------------------------------------------------------------------------
此时与上一个是类似的情况,进行了N-2次比较,交换一次
时间复杂度为O(N-1)
----------------------------------------------------------------------------
..................
因此,此时的时间复杂度为N + (N - 1) + (N - 2) + .......... + 1 = o(N2)
情况二:最坏情况2
[6,5,4,3,2,1,0]
与上一种情况类似,时间复杂度也是O(N2)
情况三:最好情况
中间为x,此时时间复杂度为O(NlogN),联系到3-way partition sort
快速排序的时间复杂度:
时间复杂度为:O(NlogN)——此时的时间复杂度是一个长期期望的一个时间复杂度
额外空间复杂度为:O(logN)
理解此时的额外空间复杂度:空间浪费在了划分点pos上
-----------------------------------
int pos = partition(a,lo,hi);
sort(a,lo,pos-1);
sort(a, pos + 1, hi);
-------------------------------------------
sort代码块中,每一次我们都需要记录pos的值,此时,当sort(a,lo,pos-1);执行完后,我们才知道右侧部分在哪儿
要注意:数组能被二分多少次,额外的空间就是多少
------------------------------------------
因此:长期的期望下:额外的空间复杂度为O(logN)
快速排序对于大量的等值存在时的优化:3-way partition
public static void sort(Comparable[] a){
if (a == null || a.length < 2) {
return;
}
StdRandom.shuffle(a);
sort(a, 0, a.length - 1);
}
private static void sort(Comparable[] a, int lo, int hi) {
if (hi <= lo) {
return;
}
Comparable v = a[lo];
int lt = lo;
int gt = hi;
int i = lo;
while (i <= gt) {
int cmp = a[i].compareTo(v);
if(cmp < 0) exch(a, lt++, i++);
else if(cmp > 0) exch(a, i, gt--);
else i++;
}
sort(a,lo,lt-1);
sort(a,gt+1,hi);
}
private static void exch(Comparable[] a, int i, int j) {
Comparable tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}
快速排序的思考:
有些算法在设计的过程中,我们想要绕过本身的数据状况怎么办?
** 有两个主要的做法:
** ①随机打乱数据状况,那么此时的时间复杂度就是一个概率事件,可能好可能坏
** ②利用哈希进行打乱
三个O(NlogN)的算法:①QickSort ②MergeSort ③HeapSort
最常用的就是快QuickSort:
* 常数项很低
在工程上:其实并不常见递归函数的存在,通常会改为非递归的版本
堆排序——HeapSort
public static void sort(Comparable[] a){
int N = a.length;
//构建二叉堆——时间复杂度为O(N)
for (int k = N/2; k >= 1; k--) {
sink(a,k,N);
}
//进行排序——时间复杂度为:O(NlogN)
while(N > 1){
exch(a,1,N);
sink(a,1,--N);
}
}
/*
- 将父节点与较大的的子节点进行交换
- */
private static void sink(Comparable[] a, int k, int N) {
while(2 * k <= N){
int j = 2 * k;
if(j < N && less(a,j,j+1)) j = j + 1; //j指向较大的字节点
if(!less(a,k,j)) break;
exch(a,j,k); //交换父节点和较大字节点
k = j; //k继续向下进行判断
}
}
private static void exch(Comparable[] a, int i, int j) {
i--;
j--;
Comparable v = a[i];
a[i] = a[j];
a[j] = v;
}
private static boolean less(Comparable[] a, int i, int j) {
return a[--i].compareTo(a[--j]) < 0;
}
时间复杂度分析:
此时利用的是堆这种数据结构:二叉堆
——堆有序的完全二叉树称为二叉堆
——二叉树的高度为logN
堆排序中有两个关键的步骤:
①构建二叉堆——时间复杂度为O(N)
* 代码:
--------------------------------
for (int k = N/2; k >= 1; k--) {
sink(a,k,N);
}
---------------------------------
* 时间复杂度:
----------------------------------------------------------------------------
每一次sink,都是对一条链进行操作,而链的长度是当前堆的高度
因为叶子节点本身就是堆有序的,即有N/2个不需要进行sink
而对于剩下的1-N/2,都需要sink,时间复杂度为:
log(N-1) + log(N - 2) + log(N - 3) + ............ + log(N - N/2)
即此时的时间复杂度为O(N)
----------------------------------------------------------------------------
②进行排序——时间复杂度为:O(NlogN)
* 代码:
--------------------------------
while(N > 1){
exch(a,1,N);
sink(a,1,--N);
}
-------------------------------
* 时间复杂度:
------------------------------------------------------------------------------------------------------
本身sink(1)的时间复杂度为logN,但是每一次数组的最后都是存放最大值,下次便不再参与比较,每一次搞定的都是末尾的数
总结:堆排序时间复杂度为:O(NlogN),额外空间复杂度为:O(1)
堆是一种很重要的数据结构
排序的稳定性及其汇总
排序的稳定性指的是:排序后相同的数在原始数组中的相对位置不变
几大排序算法的稳定性
第一类:时间复杂度为O(N2)的排序算法
①冒泡排序:
——可以实现排序稳定性
* 相同的值不进行交换
②插入排序:
——可以实现排序稳定性
* 向前插入时,遇到相同的值则停止
③选择排序:
——不可以实现排序稳定性
* 例如:5,5,5,4,0,1
此时选择最小的0与第一位5进行交换,此时就已经破坏了稳定性
第二类:时间复杂度为O(NlogN)的排序算法
④归并排序:
——可以实现排序稳定性
* 最后merge中:当左边和右边相等时,就拷贝左边的,就可以保证排序稳定性
⑤快速排序:
——不可以实现排序稳定性
* 因为partition过程无法做到排序稳定性,partition item总是a[lo],可能就导致相同的数顺序被打乱
⑥堆排序
——不可以实现稳定性
* 构建大顶堆的时候排序稳定性就已经被破坏
为什么要追求排序的稳定性?
答:实际业务中,希望原始信息不被抹去
1、如果只是简单的进行数字的排序,那么稳定性将毫无意义。
2、如果排序的内容仅仅是一个复杂对象的某一个数字属性,那么稳定性依旧将毫无意义(所谓的交换操作的开销已经算在算法的开销内了,如果嫌弃这种开销,不如换算法好了?)
3、如果要排序的内容是一个复杂对象的多个数字属性,但是其原本的初始顺序毫无意义,那么稳定性依旧将毫无意义。
4、除非要排序的内容是一个复杂对象的多个数字属性,且其原本的初始顺序存在意义,那么我们需要在二次排序的基础上保持原有排序的意义,才需要使用到稳定性的算法,例如要排序的内容是一组原本按照价格高低排序的对象,如今需要按照销量高低排序,使用稳定性算法,可以使得想同销量的对象依旧保持着价格高低的排序展现,只有销量不同的才会重新排序。(当然,如果需求不需要保持初始的排序意义,那么使用稳定性算法依旧将毫无意义)。
介绍一下工程中的综合排序算法
常见的综合排序:
----------------------------------------------------------------------------
Ⅰ 若数组长度很长,则在工程上:
* 首先进行判断:是基本类型还是自定义类型
** 如果为基础类型:则使用快速排序,因为不需要考虑数据稳定性
** 如果为自定义类型,则使用的是归并排序,因为需要考虑数据稳定性
----------------------------------------------------------------------------
Ⅱ 若数组长度短,则直接使用插入排序,不管是基本数据类型还是自定义类型
虽然插入排序时间复杂度为:O(N2),但是在数据量很小的情况下,劣势不会有很大表现
----------------------------------------------------------------------------
数组长度小于60时,直接使用插入排序
此时联系:快排和归并在数据量小的时候的优化
面试:为什么在综合排序中数据量小的部分选择使用插入排序
答:虽然复杂度高,但是常数项很低,在数据量很小的情况下,劣势不会有很大表现
只有在数据量很大的时候,常数项才可以被忽略,插入排序的劣势才表现出来
面试:为什么基本类型选择使用快排,而自定义类型选择归并
答:考虑到的是数据稳定性问题,快排因为partition数据稳定性得不到保证
而归并排序是可以设计为排序稳定性的
面试:数组中:将数组中奇数放左边,偶数放右边,要求原始数组相对次序不变,且时间复杂度为O(N),空间复杂度为O(1)
答:不能,因为此时奇数偶数也是一个0/1问题,对于快速排序中也是0/1标准,而快速排序的时间复杂度为O(NlogN)
0/1 stable sort很难
认识比较器的作用
Arrays.sort(stus,new IdAscendingComparator());
Arrays方法可以传入一个数组和自定义的比较器
利用到比较器的集合:
PriorityQueue TreeMap