认识复杂度和简单排序算法
常数时间操作
int a =arr[i];
是一个常数操作
int b=list.get(i);
不是一个常数操作,为了得到b的值只能从左到右进行遍历,逻辑上是一个线性表示,但实际上并不是线性的。
加减乘除等运算都是常数操作,和数据量无关的操作可以称为常数操作,即不管数据量的多少,每次都是固定时间完成
评价一个算法的好坏,先看时间复杂度的指标,然后在分析不同数据下的实际运行时间,也就是“常数项时间”。时间复杂度按最差情况。
选择排序
一个无序的数组,从第0个开始选择最小的与第0个交换,再从第1个开始选择第二小的交换,以此类推。
时间复杂度O(N²)空间复杂度O(1)
插入排序
一个无序的数组,从第1个数开始,与第0个数比较,构成一个有序序列,再将第2个数插入前面的有序序列。
时间复杂度O(N²)空间复杂度O(1)
二分法求局部最小
在无序的arr数组中寻找一个局部最小数。(arr相邻元素一定不相等)
局部最小指的是,该数既小于左边的数又小于右边的数
首先判断两个端点是否符合局部最小,若都不符合则其中一定存在局部最小的数
利用二分法从数组最中间的数先开始判断,若不符合,则判断是哪边不符合,若左边不符合,则左边二分,一定可以找到一个局部最小的数。
求中点:
求L…R的中点
int mid =L+((R-L)>>1); 而不采用(L+R)/2;
这么写的原因是:如果数组的长度特别大,(L+R)存在溢出可能,可能会算出负的下标
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SD8G21gc-1664459950309)(C:\Users\李一昕\AppData\Roaming\Typora\typora-user-images\image-20220923213931188.png)]
对数器
是自定义方法,比线上测试更加可靠
1.有一个你想测试的方法a
2.实现复杂度不好但是容易实现的方法b
3.实现一个随机样本产生器
4.把方法a和方法b跑相同的的随机样本,看看得到的结果是否一致
5.如果有一个随机样本使得比对结果不一致,打印样本进行人工干预,改对方法a或者方法b
6.当样本数量很多而且比对测试依然正确,则说明方法a已经正确。
递归问题的时间复杂度
剖析递归行为和递归行为时间复杂度的估算
master公式的使用
只要是满足子问题等规模的都可以用master公式
T(N)(母问题的数据量为N)=a(调用次数)*T(N/b)(子问题的数据量规模)+O(N^d)(除了子问题的调用剩余过程的时间复杂度)
static int process(int arr[],int l,int r){//母问题,令规模为N
if(l==r){
return arr[l];
}
int mid=l+((r-l)>>1);
int leftMax=process(arr,l,mid); //子问题规模为N/2,
int rightMax=process(arr,mid+1,r);//调用了两次子问题
return Math.max(leftMax,rightMax);
}
比如上述递归问题:在数组中找最大的数
T(N)=2*T(N/2)+O(1);
满足master公式求时间复杂度
logb a <d O(N^d)
logb a>d O(N^(logb a))
logb a==d O(N^d*logN)
经计算上述递归代码的时间复杂度为O(N)
归并排序
采用递归方法
先让左侧部分排好序,再让右侧部分排好序,利用辅助数组,比较左右两边的第一个数,哪个小,就将哪个放到辅助数组中,指针加加,继续比较。
public void process(int [] arr,int L,int R){
if(L == R){
return;
}
int mid = L + ((R-L) >> 1); //中点
process(arr,L,mid);
process(arr,mid+1,R);
merge(arr,L,mid,R); //外排序
}
//外排序
public void merge(int [] arr ,int l,int m,int r){
int [] temp = new int[(r-l)+1]; //空间在使用完毕会自动释放
int i = 0;
int p1 = l; //左半部分的指针
int p2 = m+1; //右半部分的指针
while (p1 <= m && p2 <= r){
temp[i++] = arr[p1] <= arr [p2] ? arr[p1++] : arr[p2++];
}
//下面两个条件只会有一个条件满足
while(p1<=m){
temp[i++] = arr[p1++];
}
while(p2<=r){
temp[i++] = arr[p2++];
}
for (i = 0; i <temp.length ; i++) {
arr[l+i] = temp[i];
}
}
时间复杂度,利用master计算为O(N*logN)
优点
没有浪费大量的时间在比较上,比前两种方法的时间复杂度小
拓展小和
public int process(int [] arr, int l ,int r){
if (l == r){ //分到不能再分就说明没有小和
return 0;
}
int mid = l + ((r-l) >> 1);
//左组小和+右组小和+排序小和
return process(arr,l,mid)
+process(arr, mid+1,r)
+merge(arr,l,mid,r);
}
public int merge(int[] arr, int l, int m, int r) {
int [] temp = new int[(r-l)+1];
int i = 0;
int p1 = l;
int p2 = m+1;
int result = 0; //小和
while (p1 <= m && p2 <= r){
//一定要排序,这样就可以通过下标的方式知道右边有多少个比arr[p1]大的数
result += arr[p1] < arr [p2] ? (r-p2+1)*arr[p1] : 0;
//相等的时候要保证右组先拷贝
temp[i++] = arr[p1] < arr [p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= m){
temp[i++] = arr[p1++];
}
while (p2 <= r){
temp[i++] = arr[p2++];
}
for (i = 0; i <temp.length; i++) {
arr[l+i] = temp[i];
}
return result;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sAxLtwRr-1664459950311)(C:\Users\李一昕\AppData\Roaming\Typora\typora-user-images\image-20220925193840591.png)]
在左右两边有相同数的时候要确保先拷贝右边的,如图中的2,这样才知道右边有几个数是大于左边的2
拓展逆序数
public class InvertedSequence {
public static void main(String[] args) {
int arr[]={5,4,3,2,1};
System.out.println( process(arr,0,4));
}
private static int process(int arr[],int l,int r){
if(l==r){
return 0;
}
int mid = l + ((r-l) >> 1);
return process(arr,l,mid)
+process(arr, mid+1,r)
+merge(arr,l,mid,r);
}
private static int merge(int arr[],int l,int m,int r){
int [] temp = new int[(r-l)+1];
int i = 0;
int p1 = l;
int p2 = m+1;
int result = 0; //统计次数
while (p1 <= m && p2 <= r){
if(arr[p1]>arr[p2]){
result+=(r-p2+1);
}
temp[i++] = arr[p1] > arr [p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= m){
temp[i++] = arr[p1++];
}
while (p2 <= r){
temp[i++] = arr[p2++];
}
for (i = 0; i <temp.length; i++) {//改变数组顺序
arr[l+i] = temp[i];
}
return result;
}
快速排序
快排前身:荷兰国旗问题
递归解决
荷兰国旗:
public static int[] process(int[] arr, int l, int r, int num) {
int less = l - 1;
int more = r + 1;
while (l < more) {//当
if (arr[l] < num) {
swap(arr, ++less, l++);//这里是less+1和l交换
}else if (arr[l] > num) {
swap(arr, l, --more);//这里的l不可以++,因为换过来的数还没有参与
}else {//相等情况
l++;
}
}
return new int[] {less + 1, more - 1};
}
private static void swap(int[] arr, int i, int j) {//交换
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
快速排序1.0
1.0和2.0的不同之处在于:如2.0图所示,1.0是将数组分成了两个模块,左边是小于等于最后一个数,右边是大于最后一个数,然后将左边的第一个数和最后一个数(标准)交换,使得标准位置正确
快速排序2.0
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jzcOJQvu-1664459950312)(C:\Users\李一昕\AppData\Roaming\Typora\typora-user-images\image-20220925213038291.png)]
时间复杂度会随着划分值而变化,划分值差不多在数值中间最好,越偏时间复杂度越大
时间复杂度为O(N^N)
快速排序3.0
将标准随机化,不以最后一个为基准
时间复杂度计算出为O(N^logN)空间复杂度为O(logN)
所需的额外空间是用来记录中点,左边的记录完可以分给右边的使用
将随机标准与最后一个数做交换,过程与2.0类似
堆
在逻辑上是一棵完全二叉树
可以用数组来表示,
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OOqoFtTI-1664459950313)(C:\Users\李一昕\AppData\Roaming\Typora\typora-user-images\image-20220927184055847.png)]
(计算父可以直接用(i-1)/2,对于右孩子两种算法的结果是一样的)
大根堆
在一个完全二叉树里,每一个子树的最大值就是头结点的值
小根堆
在一个完全二叉树里,每一个子树的最小值就是头结点的值
那么怎样把连续的数组变成一个堆?
大根堆的构造:heapInsert过程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GQ9bnChe-1664459950313)(C:\Users\李一昕\AppData\Roaming\Typora\typora-user-images\image-20220927185728926.png)]
while(arr[index] > arr[(index-1)/2]) {
swap(arr[index],arr[(index-1)/2]);
index = (index-1)/2; }
heapify
如果将一个已经排好的大根堆的头结点去掉,怎么保证他还是一个大根堆?
我们可以将最后一个结点移到头结点,将heapsize-1,在左右结点挑选一个较大的,与头结点比较,若头结点较大,则不需在进行改动,若子结点较大则交换,并重复比较其左右节点,并判断是否进行交换
public static void heapify(int arr[],int index,int heapsize)
{
int left = index * 2 + 1;
while(left < heapsize)
{
int largest = left + 1 < heapsize && arr[left]<arr[left + 1] ?
left + 1:left;
largest = arr[largest] > arr[index] ? largest : index;
if(largest == index)
break;
swap(arr[largest],arr[index]);
index = largest;
left = index * 2 + 1;
}
}
堆里最重要的两个方法:heapinsert heapify
思考:
若将数组中的一个数换成随机数,怎么保证其还是一个大根堆?
我们可以对该数进行heapinsert方法,若能执行则说明换了一个比原先较大的数,并保证其大根堆的性质。
可以对该数进行heapify方法,若能执行则说明换了一个比原先较小的数,并保证其大根堆的性质。
用户移除一个数,并将其调整成大根堆的过程是logN级别的O(logN),
堆排序
给一个数组,先让其变成一个大根堆,而大根堆的头结点一定是数组中最大的数,将其与最后一个节点交换,将除最后一个节点外的其余数变成大根堆,重复操作
若只进行大根堆变换
对于一个数组要进行大根堆变换,我们可以直接对倒数第二层进行heapify操作,时间复杂度为O(N)
堆排序拓展
题目:已知一个几乎有序的数组,几乎有序是指,如果把数组排好顺序的话,每个元素移动的距离可以不超过k,并且k相对于数组来说比较小,请选择一个合适的排序算法针对这个数据进行排序
题目关键点: 几乎有序
我们可以利用小根堆,建立一个k+1长度的小根堆,0-(k+1)范围中一定包含该数组中的最小值,1-(k+2)范围中一定包含第二小值,依次后推。
PriorityQueue<Integer>heap=new PriorityQueue<>();
//在java中这就是堆结构,他的构造方法在不传任何东西下就是小根堆
//关于扩容问题
// PriorityQueue 是一个无界队列,但是初始的容量(实际是一个Object[]),随着不断向优先级队列添加元素,其容量会自动扩容(成倍扩容)
//相当于一个黑盒,不支持已经成了堆的再次对其改变并让其成堆,不要对其内部进行改变,这种情况需要手写堆,若只需要对其进行堆排序则可直接使用
heap.add(3);
heap.add(2);
heap.add(1);
heap.add(9);
heap.add(4);
while (!heap.isEmpty()){//利用小根堆将最小值依次弹出
System.out.println(heap.poll());
}
PriorityQueue<Integer> heap = new PriorityQueue<Integer>();
int index = 0;
for (; index <= Math.min(arr.length - 1, k); index++) {
heap.add(arr[index]);//1.先将前k+1个数放到小根堆里,
}
int i = 0;
for (; index <= arr.length - 1; i++, index++) {
heap.add(arr[index]); //3.加一个数放到小根堆
arr[i] = heap.poll(); //2.弹一个数放到数组
}
// 弹出剩余数
while (!heap.isEmpty()) {
arr[i++] = heap.poll();
}
比较器
Arrays.sort(默认排序);
外部比较器–
Comparator
接口位于java.util
包下。
Comparator
接口是一个跟Comparable
接口功能很相近的比较器。比较大的区别是,实现该接口的类一般是一个独立的类。详情看代码:
import java.util.*;
class Employee{
private String name;
private int age;
private long salary;
public Employee(String name,int age,long salary){
this.salary=salary;
this.name=name;
this.age=age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public long getSalary() {
return salary;
}
@Override
public String toString() {
return this.name+"\t"+this.age+"\t"+this.salary;
}
}
//创建年龄比较器:
class AgeComparator implements Comparator<Employee>{
//比较器默认:返回负数的时候,第一个参数排在前面
//返回正数的时候,第二个参数排在前面
//返回0的时候谁在前面无所谓
@Override
public int compare(Employee o1, Employee o2) {
if (o1.getAge()>o2.getAge()){
return 1;
}else if(o1.getAge()<o2.getAge()){
return -1;
}else {
return 0;
}
//上述比较等同于:
return o1.getAge()-o2.getAge();
}
}
//创建薪水比较器:
class SalaryComparator implements Comparator<Employee>{
@Override
public int compare(Employee o1, Employee o2) {
if (o1.getSalary() > o2.getSalary()) {
return 1;
} else if (o1.getSalary() < o2.getSalary()) {
return -1;
} else {
return 0;
}
}
}
class RunClass {
public static void main(String[] args) {
Employee[] ems = {
new Employee("zhansan", 26, 30000),
new Employee("lisi",14, 24000),
new Employee("laowang",40, 10000)
};
System.out.println("===============使用薪水比较器来进行排序");
//排序前
for(Employee e:ems){
System.out.println(e.toString());
}
Arrays.sort(ems,new SalaryComparator()); //使用薪水比较器进行排序,ems指的是需要排序的数组,SalaryComparator()指的是排序的方法
System.out.println("==============");
//排序后
for(Employee e:ems){
System.out.println(e.toString());
}
System.out.println("===============使用年龄比较器来进行排序");
List<Employee> myList=new ArrayList<Employee>();
myList.add(new Employee("zhansan", 23522, 20000));
myList.add(new Employee("lisi", 23436, 24000));
myList.add(new Employee("lisi", 23436, 24000));
//排序前
for(Employee e:ems){
System.out.println(e.toString());
}
Collections.sort(myList,new AgeComparator()); //使用年龄比较器进行排序
System.out.println("==============");
//排序后
for(Employee e:ems){
System.out.println(e.toString());
}
}
}
(1)比较器的实质就是重载比较运算符
(2) 比较器可以很好地应用在特殊标准的排序上,比如说按照年龄大小
(3) 比较器可以很好地应用在根据特殊标准排序的结构上,比如堆排序
不基于比较的排序
根据数据状况排序
基数排序
可以利用队列数组栈等
几进制就需要几个桶
按照最大的位数补全代码,比如13补全为013
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XiefTWzC-1664459950315)(C:\Users\李一昕\AppData\Roaming\Typora\typora-user-images\image-20220928202450038.png)]
越高位数越晚排序,优先级越高
public static int maxbits(int[] arr) {
int max = Integer.MIN_VALUE;
for (int i = 0; i < arr.length; i++) {
max = Math.max(max, arr[i]); //找出数组的最大值
}
int res = 0;
while (max != 0) {
res++;
max /= 10;//看最大数是几位数
}
return res;
}
public static void radixSort(int[] arr, int begin, int end, int digit) {//这里的digit就是最大位数
final int radix = 10;//radix不能被重写或者重载
int i = 0, j = 0;
for (int d = 1; d <= digit; d++) {//有多少位数就发生几次进出桶
int[] count = new int[radix];//利用count数组中的数将排序数字放到bucket数组
int[] bucket = new int[end - begin + 1];//存放排好序的数字
for (i = begin; i <= end; i++) {
j = getDigit(arr[i], d);//d位数字对应的桶加加,处理成前缀和
count[j]++;
}
for (i = 1; i < radix; i++) {
count[i] = count[i] + count[i - 1];//将每个桶变成小于等于这个下标的有多少个数
}
for (i = end; i >= begin; i--) {//从最后一个数开始,count[j]表示有几个数小于等于j,将最后一个数放到count[j]-1的位置上,以此类推
j = getDigit(arr[i], d);
bucket[count[j] - 1] = arr[i];
count[j]--;
}
for (i = begin, j = 0; i <= end; i++, j++) {
arr[i] = bucket[j];//维持出桶结果
}
}
}
public static int getDigit(int x, int d) {
return ((x / ((int) Math.pow(10, d - 1))) % 10);//计算d位的数
}
排序算法的稳定性
不具备稳定性的排序:
选择排序、快速排序、堆排序
具备稳定性的排序:
冒泡排序、插入排序、归并排序、一切桶排序思想下的排序。 //不基于比较的排序容易做到稳定性
目前还没有找到时间复杂度为O(logN*N),额外空间复杂度为O(1),又稳定的排序
我们需要学习稳定性的原因:
比如在生活中,我们想要购买一件商品,先按价格排序,再按好评排序,这样我们会得到物美价廉的商品
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hafY8ioB-1664459950316)(C:\Users\李一昕\AppData\Roaming\Typora\typora-user-images\image-20220929213434695.png)]
结论:
归并的劣势在于空间复杂度较高,但是稳定
快排的优势是常数项低,是跑得最快的排序,空间复杂度略高,也无法稳定
堆排的优势是空间使用的很小
那么基于比较的排序不能做到比O(N*logN)低
目前也没有时间复杂度在O(N*logN)且空间复杂度在O(N)以下
所以不同的排序有各自的优点和劣势,应根据需要选择
常见的坑:
1.归并排序的空间复杂度是可以变成O(1),但是不稳定。毫无用处
2.原地归并排序,空间复杂度变低,但是时间复杂度变成O(N²) 更无用处
3.快速排序可以做到稳定性,但空间复杂度会提高 无用
4.有一道题目,是奇数放在数组的左边,偶数放在数组的右边,还要求原始的相对次序不变,不使用额外空间,且时间复杂度为O(N),这个问题和快排的01问题一样,大于某个数放左边,小于某个数放右边,而快排并不能保持稳定性。这个问题可以实现,但是非常难,属于论文级别
工程上对排序的改进
1)充分利用O(N*logN)和O(N²)的各自的优势
比如在快排中,如果是小样本量,我们可以选择插入排序,可以将两个排序拼在一起,当划分到小样本,利用插入排序更快。
2)稳定性的考虑
对于Arrays.sort 在系统内部,如果是基础类型的数据会使用快排(默认不需要稳定性),如果是自己定义的非基础类型会用归并。这就是因为稳定性。
0316)]
结论:
归并的劣势在于空间复杂度较高,但是稳定
快排的优势是常数项低,是跑得最快的排序,空间复杂度略高,也无法稳定
堆排的优势是空间使用的很小
那么基于比较的排序不能做到比O(N*logN)低
目前也没有时间复杂度在O(N*logN)且空间复杂度在O(N)以下
所以不同的排序有各自的优点和劣势,应根据需要选择
常见的坑:
1.归并排序的空间复杂度是可以变成O(1),但是不稳定。毫无用处
2.原地归并排序,空间复杂度变低,但是时间复杂度变成O(N²) 更无用处
3.快速排序可以做到稳定性,但空间复杂度会提高 无用
4.有一道题目,是奇数放在数组的左边,偶数放在数组的右边,还要求原始的相对次序不变,不使用额外空间,且时间复杂度为O(N),这个问题和快排的01问题一样,大于某个数放左边,小于某个数放右边,而快排并不能保持稳定性。这个问题可以实现,但是非常难,属于论文级别
工程上对排序的改进
1)充分利用O(N*logN)和O(N²)的各自的优势
比如在快排中,如果是小样本量,我们可以选择插入排序,可以将两个排序拼在一起,当划分到小样本,利用插入排序更快。
2)稳定性的考虑
对于Arrays.sort 在系统内部,如果是基础类型的数据会使用快排(默认不需要稳定性),如果是自己定义的非基础类型会用归并。这就是因为稳定性。