认识复杂度和简单排序算法

认识复杂度和简单排序算法

常数时间操作

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 在系统内部,如果是基础类型的数据会使用快排(默认不需要稳定性),如果是自己定义的非基础类型会用归并。这就是因为稳定性。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值