【算法导论】阶段复习(算法分析,分治,比较排序,二叉树)

本文详细介绍了算法分析中的分治思想、时间复杂度的表示(O,Θ,Ω)以及几种常见的比较排序算法(插入排序、堆排序、归并排序、快速排序)及其时间复杂度。同时提及了线性时间排序(计数排序、基数排序和桶排序)以及二叉树的概述。
摘要由CSDN通过智能技术生成

title: 阶段复习一
date: 2023-10-07 18:54:49
tags: algorithem


主要是用来应对EXAM1的笔记,也用做博客参考吧。

复习目录

  • 算法分析
  • 分治思想
  • 比较排序
  • 二叉树

算法分析

我们主要关注运行时间,运行时间中,我们主要关注增长量级。

在输入规模足够大时,运行时间主要有增长量级决定,这时我们主要研究算法的渐进效率

渐进记号

  • Θ \Theta Θ

    :严格的指出上下确界。用集合来表示就是

Θ ( g ( n ) ) = f ( n ) : 存在 c 1 , c 和 n 0 , 使得对所有的 n > = n 0 , 有 0 < = c 1 g ( n ) < = f ( n ) < = c 2 g ( n ) \Theta(g(n)) = {f(n):存在c_1,c_和n_0,使得对所有的n>=n_0,有0<=c_1g(n)<=f(n)<=c_2g(n)} Θ(g(n))=f(n):存在c1,cn0,使得对所有的n>=n0,0<=c1g(n)<=f(n)<=c2g(n)

可以想成"="。

  • O O O

:指出上确界。用集合表示就是:
O ( g ( n ) ) = f ( n ) : 存在 c 和 n 0 , 使得 n > = n 0 , 有 0 < = f ( n ) < = c g ( n ) O(g(n))={f(n):存在c和n_0,使得n>=n_0,有0<=f(n)<=cg(n)} O(g(n))=f(n):存在cn0,使得n>=n0,0<=f(n)<=cg(n)
可以想成f(n)<=g(n)的表示。而o符号表示一个不那么紧邻的上确界。

  • Ω \Omega Ω

:指出下确界。用集合表示就是:
Ω ( g ( n ) ) = f ( n ) : 存在 c 和 n 0 , 使得 n > = n 0 , 有 0 < = c g ( n ) < = f ( n ) \Omega(g(n)) = {f(n):存在c和n_0,使得n>=n_0,有0<=cg(n)<=f(n)} Ω(g(n))=f(n):存在cn0,使得n>=n0,0<=cg(n)<=f(n)
可以想成f(n)>=g(n)的表示。而小写的w表示一个不那么紧邻的下确界,例如是n<n^2的n。

分治方法(Divide and Conquer)

分治法(divide and conquer)是将问题分成规模更小的子问题,再分开解决之后合并的方法。因此主要为三步:

  • 分解:将问题分解成规模更小但相同的子问题

  • 解决:递归地解决子问题,当子问题递归到足够小或者能直接解决,则停止递归;

  • 合并:将子问题的解的集合合并成原问题的解

    归并排序,快速排序和顺序查找便是用了这一思想,留着后面介绍。

分治方法的分析

递归式

递归式可以很贴切的描述分支方法的过程,例如归并排序的运行时间用T(n)表示,那么T(n)可以递归地表示为:
T ( n ) = 2 T ( n / 2 ) + Θ ( n ) T(n) = 2{T(n/2)}+\Theta(n) T(n)=2T(n/2)+Θ(n)
求时间复杂度主要有三个方法:

  • 代入法:猜一个复杂度,然后用数学归纳法证明。
  • 递归树法:主要就是求树的高度,树的高度对于最大调用次数,主要的运行时间也取决于递归调用次数。
  • 主方法

这里主要介绍主方法。

主方法

对于形如:
T ( n ) = a T ( n / b ) + Θ ( n c ) T(n) = aT(n/b) + \Theta(n^c) T(n)=aT(n/b)+Θ(nc)
其中a>=1,b>1

case1:
l o g b a < c log_b a < c logba<c
时间复杂度O(n^c)

case2:
l o g b a = c log_b a = c logba=c
时间复杂度O(n^c*lgn)

case3:
l o g b a > c log_b a > c logba>c
时间复杂度O(n^log_b a)

简单来说,log_b a和c谁大,就是O(n^{大的那一方}),需要特别记忆的只是相等的情况。

比较排序(Comparsion Sort)

主要是以比较为排序依据的算法:

  • 插入排序
  • 堆排序
  • 归并排序
  • 快速排序(以及随机化)

插入排序

插入排序就是遍历所有的输入序列,将当前遍历的元素插入已排序的序列,插入也需要遍历已排序序列。

伪代码:

for j = 2 to A.length:
	key = A[j]
	//Insert A[j] to sorted array A[1,...,j-1]
	for i = j - 1 downto 1:
		//if the current number >key,then put current number to next position
		if A[i] > key:
			A[i+1] = A[i]
		else:
			//the psition of current number is A[j]
			A[i+1] = key
			break
	 

时间复杂度:O(n^2)

堆排序

堆排序建立了一个二叉堆,利用小根堆或者大根来维护二叉堆,将根节点和最后一个节点交换,递归地用小根堆或者大根堆来更新二叉堆的算法。

//维护小根堆
public static void heapify(double[] a,int i,int n){
	int largest = i;//初始化最大值的下标为i
	int left = 2*i+1;
	int right = 2*i+2;
	
	if(left < n&&a[left]<=a[largest]) largest = left;//更新最大值下标
	if(right < n&&a[right]<=a[largest]) largest = right;
	if(largest != i){
		swap(a[largest],a[i]);
		heapify(a,largest,n);
	}
}
public static void HeapSort(double[] a){
		n = a.length;
		for(int i = n/2-1;i>=0;i--)
			heapify(a,i,n);//建立大根堆
		
		for(int i = n-1;i>=0;i--){
			swap(a[0],a[i]);
			heapify(a,0,i);//更新交换根节点和最后的节点的堆,并且交换后的最后的节点不参与更新,因此i——
		}
}
  • 建立大(小)根堆;(从底部向顶部维护,因此是从n/2-1开始)
  • 交换保存着最大值最小的根节点;
  • 每交换一次,更新大(小)堆;
  • 大根堆输出从小到大,小根堆输出从大到小。

时间复杂度:O(nlgn)。

归并排序

归并排序的思想基于分治方法,分解,解决,合并。

  • 分解:找到每段序列的中点

  • 解决:对足够小的序列进行排序,递归地调用

  • 合并:将排序好的子序列进行合并

    循环不变式为:- 利用端点求中点-更新端点-处理左边

    ,递归处理右边-利用当前端点归并排序

    public static void progress(double[] a,int l,int r){
    	if(l == r) return;
    	int mid = l + (r-l)/2;//防止溢出
    	progress(a,l,mid);
    	progress(a,mid+1,r);
    	MergeSort(a,l,mid,r);
    }
    public static void MergreSort(double[] a,int l,int r){
    	int[] arr = new int[r-l+1];
    	int p = mid + 1//右半的指针
    	int k = 0;//新数组的计数器
    	while(l<=mid&&p<=r){
    		if(a[l] < a[p]){
    			arr[k++] = a[l++];
    		}elif(a[l] > a[p]){
    			arr[k++] = a[p++];	
    		}
    	}//较小的存入新数组
    	while(l<=mid)a[l++]//如果无操作(包括元素之间相等和某一方遍历完成)arr[k++] = a[l++];
    	while(p<r)a[p++]arr[k++] = a[p++];
    	for(int i=l;i <= r;i++) a[i] = arr[k-l];//转存回去
    }
    

时间复杂度O(nlgn)。

快速排序

快速排序和归并类似,不过快速排序不需要转存,是在原本数组上进行的“原址排序”。

循环不变式为:- 更新划分-快速排序当前划分下的左半-快速排序当前划分下的右半。

public static void QuickSort(double[] a,int l,int r){
	int p = Parrtition(a,l,r);
	QuickSort(a,l,p);
	QuickSort(a,p+1,r);
}
public static void Parrtition(double[] a,int l,int r){
	x = a[r];
	p = l - 1;
	for(int i = 1;i<r;i++){
		p++;
		if(a[i] < x){
			swap(a[p],a[i]);
		}
	}
	swap(a[p+1],a[r]);
}

时间复杂度为O(nlgn),在划分为0和n-1的情况下退化为O(n^2)(T(n)=T(n-1)+O(n))。

快速排序的随机化版本主要在选取主元时采用了随机化:

public static void RandomParrtition(double[] a,int l,int r){
	tmp = random.nextint(r-1)+l;
	swap(a[tmp],a[r]);
	return Parrtition(a,l,r);
}

随便找一个下标和最右边的交换即可.

时间复杂度为O(nlgn)。

比较排序的时间下界

根据决策树模型,对于n个元素排序,树的高度h对应调用次数,所有比较情况最多是一棵满二叉树的叶子节点的数量:2^h;所有比较情况至少是一颗完全二叉树的叶子节点的数量:n!,那么有了调用次数和n的关系:
2 h > n ! 2^h > n! 2h>n!
通过斯特林公式可以大致得到时间复杂度为:
Ω ( n l g n ) \Omega(nlgn) Ω(nlgn)
所有的比较排序的时间下界就是这个。

归并和堆排序是渐进最优的比较排序,是稳定的。

线性时间的排序(Linear time Sort)

主要 是在线性时间O(n)完成的排序算法:

  • 计数排序
  • 基数排序
  • 桶排序

计数排序

计数排序主要是对范围0k的数组排序,为0k的每个数字找到一个计数器,遍历一遍,出现一次这个数字变计数一次,最后将计数器求前缀和,找到每一个数的位置。

public static double[] CountingSort(double[] a){
	double[] c = new double[max(a)];
	double[] b = new double[a.length];
	
	for(int i = 0;i < a.length;i++){
		x = a[i];
		c[x]++;
	}
	for(int i = 1;i <= max(a);i++){
		c[i] = c[i-1]+c[i];
	}
	for(int i = 0;i < a.length;i++){
		x = a[i];;
		b[c[x]] = a[i];
		c[x]--;
	}
	return b;
}

时间复杂度:O(n)。

空间复杂度:O(n)。

基数排序

基数排序就是从最低有效位开始排序,到最高有效位,一共进行d轮排序,d是该数的位数,每一次排序应该是稳定排序,因为要保证下一次排序时,这一位相同的数字的相对位置不变。

public class RadixSort {
// 获取数组中最大的数
    public static int getMax(int[] arr) {
        int max = arr[0];
        for (int i = 1; i < arr.length; i++) {
            if (arr[i] > max) {
                max = arr[i];
            }
        }
        return max;
    }

    // 使用计数排序对数组按照指定的位数进行排序
    public static void countingSort(int[] arr, int exp) {
        int n = arr.length;
        int[] output = new int[n];
        int[] count = new int[10];
        Arrays.fill(count, 0);

        // 统计每个数字的出现次数
        for (int i = 0; i < n; i++) {
            count[(arr[i] / exp) % 10]++;
        }

        // 计算每个数字应该在输出数组中的位置
        for (int i = 1; i < 10; i++) {
            count[i] += count[i - 1];
        }

        // 构建输出数组
        for (int i = n - 1; i >= 0; i--) {
            output[count[(arr[i] / exp) % 10] - 1] = arr[i];
            count[(arr[i] / exp) % 10]--;
        }

        // 将输出数组复制到原始数组中
        for (int i = 0; i < n; i++) {
            arr[i] = output[i];
        }
    }

    // 使用基数排序对数组进行排序
    public static void radixSort(int[] arr) {
        int max = getMax(arr);

        // 从最低位到最高位依次进行排序
        for (int exp = 1; max / exp > 0; exp *= 10) {
            countingSort(arr, exp);
        }
}**

时间复杂度位O(n);

桶排序

桶排序是将元素放入不同桶中,再对桶内元素进行排序。跟散列表(hash table)有类似之处。

public static List<integer>[] BucketSort(double[] a){
	int n = a.length;
	List<integer>[] b = new List[n];//表示一个泛型数组,其中每个元素都是一个 List 对象,该 List 对象中存储的元素类型为 Integer
	for(int i = 0;i < n;i++){
		b[i] = new ArrayList<>;
	}
    for(i = 0;i < n;i++){
    	b[(int)n*a[i]].add(a[i]); 
    }
    for(int i = 0; i < n;i++){
    	b[i].sort();
    }
    return b;
}

二叉树(Binary Search Tree)

单独讲

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值