算法分析与设计复习

第一章-绪论

什么是算法
  • 算法定义:算法是一系列解决问题的明确指令,也就是说,对于符合一定规范的输入,就能在有限时间内获得要求的输出

第二章-算法效率分析基础

Ο-小于等于
  • 读作O
Θ-等于
  • 读作(theta)
Ω-大于等于
  • 读作(omega)
常见函数值大小排序

n ! > 2 n n!>2^n n!>2n > n 3 > n 2 > n log ⁡ n > log ⁡ n > n >n^3>n^2>n\log n>\log n>n >n3>n2>nlogn>logn>n

第三章-蛮力法问题的描述

  • 一种简单直接地解决问题的方法,常常直接基于问题的描述和所涉及的概念定义。
  • 力是指计算机计算的能力
选择排序与冒泡排序(√)
  • 选择排序(每从第 i 个元素开始扫描,扫描一次找到第 i 小/大的元素,和第 i 个元素进行交换)
  • 冒泡排序(比较相邻位置两个元素,如果是逆序则交换它们的位置,一轮数组遍历下来,最大/小的元素就会到最后一个位置,第二次则将第二大/小的元素放到倒数第二个位置)
//非伪随机,去注释即可用 
#include <stdio.h>
#include <stdlib.h>
#include <iostream> 
#include <time.h>
using namespace std;

void  DisplayArray(int *A, int length);

/*选择排序 
*思想 :每从第 i 个元素开始扫描,扫描一次找到第 i 小/大的元素,和第 i 个元素进行交换
*/
void  ChooseSort(int *A,int length){
	for(int i = 0; i<length; i++){
		int temp_max = i;
		for(int j = i; j<length; j++){
			if(A[j] >= A[temp_max]){
				temp_max = j;
			}
		}
		int temp_change = A[i];
		A[i] = A[temp_max];
		A[temp_max] = temp_change;	
	}
	cout<<"选择排序"<<endl;
	DisplayArray(A,length);
}

/*冒泡排序 
*思想 :比较相邻位置两个元素,如果是逆序则交换它们的位置,一轮数组遍历下来,最大/小的元素就会到最后一个位置,第二次则将第二大/小的元素放到倒数第二个位置
*/
void  BubbleSort(int *A,int length){
	for(int i = 0; i<length; i++){
		for(int j = 0; j+1<length-i; j++){ 
			if(A[j] < A[j+1]){
				int temp_change = A[j];
				A[j] = A[j+1];
				A[j+1] = temp_change;
			}
		}			
	}
	cout<<"冒泡排序"<<endl;
	DisplayArray(A,length);
}

void  DisplayArray(int *A, int length){
	cout<<"数组打印:"<<endl;
	for(int i = 0; i<length; i++){
		cout<<A[i]<<" ";
	}
	cout<<endl;
} 

int main()
{
    srand(time(NULL));
    //printf ("%d\t",rand()%33);
    int length = rand()%30;
    while(length<=2){
		length = rand()%30;
	}
	int array[length];
	for(int i = 0; i<length; i++){
		array[i] = rand()%100;
	}
	DisplayArray(array,length);
	ChooseSort(array,length);
	BubbleSort(array,length);
	
    return 0;
}
  • 选择排序算法分析
    • 输入规模:n(长度为n的数组)
    • 基本操作:if(A[j] >= A[temp_max])
    • 是否与具体输入有关:无关(就算是输入已排好序的一个数组,每一轮也要经过这么多次比较最终挑出本轮最大)
键的比较次数基本操作执行次数时间复杂度空间复杂度稳定性在位性
平均情况 C ( n ) = ∑ i = 0 n − 2 ∑ j = i + 1 n − 1 1 C(n) = \sum_{i=0}^{n-2}{\sum_{j=i+1}^{n-1}} 1 C(n)=i=0n2j=i+1n11Θ( n 2 n^2 n2)没有用到额外辅助空间不稳定在位

执行次数(求和表达式结果): C ( n ) = ∑ i = 0 n − 2 [ ( n − 1 ) − ( i + 1 ) + 1 ] = ∑ i = 0 n − 2 ( n − 1 − i ) = n ( n − 1 ) 2 C(n)=\sum_{i=0}^{n-2}[(n-1)-(i+1)+1]=\sum_{i=0}^{n-2}(n-1-i) =\frac{n(n-1)}{2} C(n)=i=0n2[(n1)(i+1)+1]=i=0n2(n1i)=2n(n1)

结论:对于任何输入,选择排序都是Θ( n 2 n^2 n2)的算法。
优点:键的交换次数仅为Θ( n n n),精确说是 n − 1 n-1 n1次,这个特点使得选择排序优于其他许多排序算法。

  • 冒泡排序算法分析
    • 输入规模:n(长度为n的数组)
    • 基本操作:if(A[j] < A[j+1])
    • 是否与具体输入有关:有关()
    • 键的比较次数与选择排序相同,但是交换次数平均情况和最坏情况都是 Θ ( n 2 ) \Theta(n^2) Θ(n2)
    • 是一个垃圾排序算法,除了名字可爱一无是处。
键的比较次数基本操作执行次数时间复杂度空间复杂度稳定性在位性
平均情况 C ( n ) = ∑ i = 0 n − 2 ∑ j = i + 1 n − 1 1 C(n) = \sum_{i=0}^{n-2}{\sum_{j=i+1}^{n-1}} 1 C(n)=i=0n2j=i+1n11Θ( n 2 n^2 n2)没有用到额外辅助空间不稳定在位
键的交换次数最大次数时间复杂度空间复杂度稳定性在位性
平均 / 最坏情况 C ( n ) = n ( n − 1 ) 2 ϵ Θ ( n 2 ) C(n) = \frac{n(n-1)}{2}\epsilon \Theta(n^2) C(n)=2n(n1)ϵΘ(n2) Θ ( n 2 ) \Theta(n^2) Θ(n2)不稳定在位
蛮力字符串匹配
最近对问题

第四章-减治法

求解问题思路,找整个问题的解和子问题的解
  • 定义:利用一个问题给定实例的解和同样问题较小实例的解的某种关系
    • 自顶向下求解:通常递归
    • 自底向上求解:通常迭代(非递归比较好)
  • 三种:
    • 减常量(通常减一法):插入排序,拓扑排序
    • 减常因子(通常减 1 2 \frac{1}{2} 21,俗称减半法):二分检索,假币问题
    • 减可变规模:欧几里得算法(辗转相除法求最大公约数),选择算法
  • 区分减半法与分治法:(以计算 a n a^n an为例)
    • 减一法: a n = a ∗ a n − 1 a^n=a*a^{n-1} an=aan1
    • 分治法 a n = a n 2 ∗ a n 2 a^n=a^{\frac{n}{2}}*a^{\frac{n}{2}} an=a2na2n
      • F ( a , n ) = { r e t u r n ( a ) , n = 1 r e t u r n ( F ( a , n 2 ) ∗ F ( a , n 2 ) ) , n ≠ 1 F(a,n)=\begin{cases} return(a),n=1\\ return (F(a,\frac{n}{2})*F(a,\frac{n}{2})), n\neq1\end{cases} F(a,n)={return(a)n=1return(F(a,2n)F(a,2n))n=1
      • 基本操作执行次数(迭代次数) { M ( n ) = M ( n 2 ∗ 2 + 1 ) M ( 1 ) = 0 \begin{cases} M(n)=M(\frac{n}{2}*2+1)\\ M(1)=0\end{cases} {M(n)=M(2n2+1)M(1)=0
    • 减半法 a n = a n 2 ∗ a n 2 a^n=a^{\frac{n}{2}}*a^{\frac{n}{2}} an=a2na2n(虽然递推式看不出区别,但是算法实现起来有区别)
      • F ( a , n ) = { r e t u r n ( a ) , n = 1 r e t u r n ( F ( a , n 2 ) 2 ) , n ≠ 1 F(a,n)=\begin{cases} return(a),n=1\\ return (F(a,\frac{n}{2})^2), n\neq1\end{cases} F(a,n)={return(a)n=1return(F(a,2n)2)n=1
      • 基本操作执行次数(迭代次数) { M ( n ) = M ( n 2 + 1 ) M ( 1 ) = 0 \begin{cases} M(n)=M(\frac{n}{2}+1)\\ M(1)=0\end{cases} {M(n)=M(2n+1)M(1)=0
      • 这个求解 a n a^n an的案例中,减半法的原来规模与减半后规模的关系是: 原 来 规 模 = 减 半 规 模 2 原来规模={减半规模}^2 =2
      • 如果n为奇数, a n = ( a n − 1 2 ) 2 × a a^n=(a^{\frac{n-1}{2}})^2\times a an=(a2n1)2×a
      • 算法时间复杂度为 Θ ( log ⁡ n ) \Theta(\log n) Θ(logn)
减常数(减1法)(√)
插入排序(减一法,递归)(但是迭代效率显然更高
  • 算法思想:对于一串长度为 n − 1 n-1 n1已经排好序的数组 A [ 0 , n − 2 ] A[0,n-2] A[0,n2],每次将第 n − 1 n-1 n1个元素插入到排好序的数组间形成长度为 n n n的排好序数组,插入到合适位置后,后面的元素要依次往后面挪一位。
/*插入排序 
*思想 :对于一串长度为$n-1$已经排好序的数组$A[0,n-2]$,每次将第$n-1$个元素插入到排好序的数组间形成长度为$n$的排好序数组,插入到合适位置后,后面的元素要依次往后面挪一位
*减治法-减常数法(减一法 ) 
*迭代实现
*/
void  InsertionSort(int *A,int length){
	for(int i = 1; i<length; i++){
		bool IsInserted = false;
		int temp_change = A[i];
		for(int j = 0; j<i+1; j++){ //内层循环寻找当前待插入元素的合适位置 			
			if(A[i]>A[j] && !IsInserted){
				temp_change = A[j];
				A[j] = A[i];
				IsInserted = true;
			}
			else if(IsInserted){	
				int temp_change2 = A[j];
				A[j] = temp_change;
				temp_change = temp_change2;
			} 
		}			
	}
	cout<<"插入排序"<<endl;
	DisplayArray(A,length);
}
键的比较次数基本操作执行次数时间复杂度空间复杂度稳定性在位性
最坏情况 C ( n ) = ∑ i = 1 n − 1 ∑ j = 0 i − 1 1 = ∑ i = 1 n − 1 i = n ( n − 1 ) 2 C(n) = \sum_{i=1}^{n-1}{\sum_{j=0}^{i-1}} 1=\sum_{i=1}^{n-1}i=\frac{n(n-1)}{2} C(n)=i=1n1j=0i11=i=1n1i=2n(n1)Θ( n 2 n^2 n2)额外辅助空间常数个稳定在位
最优情况 C ( n ) = ∑ i = 1 n − 1 1 = n − 1 C(n) = \sum_{i=1}^{n-1} 1=n-1 C(n)=i=1n11=n1Θ( n n n)额外辅助空间常数个稳定在位
平均情况 C ( n ) = 1 N ∑ i = n − 1 n ( n − 1 ) 2 ≈ n 2 4 C(n) =\frac{1}{N}\sum_{i=n-1}^{\frac{n(n-1)}{2}}\approx\frac{n^2}{4} C(n)=N1i=n12n(n1)4n2Θ( n 2 n^2 n2)额外辅助空间常数个稳定在位
  • 算法分析
    • 在最优输入的情况下,有非常好的性能,但是这种情况没有多大意义
    • 由于其平均情况性能比最差性能快一倍,以及在基本有序的数组中表现出来的优异能力,使得插入排序优于选择排序和冒泡排序
    • 还有一种扩展算法——希尔(Shell)排序
减常因子
折半查找及二分检索树(⭐)(√)
  • 算法思想:(前提:在有序数组中进行查找),通过比较键 K K K与数组中间元素 A [ m ] A[m] A[m]来完成查找工作,如果它们相等,则算法结束。否则,如果 K < A [ m ] K<A[m] K<A[m]则对数组前半部分执行该操作,如果 K > A [ m ] K>A[m] K>A[m]则对数组后半部分执行该操作。
  • 折半查找效率标准方法:计算找键和数组的元素的比较次数
键的比较次数基本操作执行次数(递推式)时间复杂度
最坏情况 C w r o s t ( n ) = C w r o s t ( ⌊ n 2 ⌋ ) + 1 , C w r o s t ( 1 ) = 1 C_{wrost}(n) =C_{wrost}(\lfloor \frac{n}{2} \rfloor)+1,C_{wrost}(1)=1 Cwrost(n)=Cwrost(2n)+1Cwrost(1)=1(递推式结果为 C w r o s t ( n ) = ⌊ log ⁡ 2 n ⌋ + 1 = ⌈ log ⁡ 2 n + 1 ⌉ C_{wrost}(n)=\lfloor \log_2n \rfloor+1=\lceil \log_2{n+1} \rceil Cwrost(n)=log2n+1=log2n+1Θ( log ⁡ n \log n logn)
平均情况Θ( log ⁡ 2 n \log_2n log2n)
  • 计算上述递推式详细过程:需要先采用平滑规则(即对 n n n n = 2 k n=2^k n=2k),再运用反向替换法
    • k > 0 k>0 k>0时, A ( 2 k ) = A ( 2 k − 1 ) + 1 A(2^k)=A(2^{k-1})+1 A(2k)=A(2k1)+1 A ( 2 0 ) = 1 A(2^0)=1 A(20)=1。这是经过平滑后的递推式,可以很轻松的利用反向替换法求解了。
    • 反向替换法, A ( 2 k ) = A ( 2 k − 1 ) + 1 = ( A ( 2 k − 2 ) + 1 ) + 1 = A ( 2 k − 2 ) + 2 A(2^k)=A(2^{k-1})+1=(A(2^{k-2})+1)+1=A(2^{k-2})+2 A(2k)=A(2k1)+1=(A(2k2)+1)+1=A(2k2)+2,即将 A ( 2 k − 1 ) A(2^{k-1}) A(2k1) A ( 2 k − 2 ) + 1 A(2^{k-2})+1 A(2k2)+1替换,一直替换最终得到 A ( 2 k ) = A ( 2 k − k ) + k = A ( 2 0 ) + k = 1 + k A(2^k)=A(2^{k-k})+k=A(2^0)+k=1+k A(2k)=A(2kk)+k=A(20)+k=1+k
    • 再将 n n n换回来, ∵ n = 2 k ∴ k = log ⁡ 2 n \because n=2^k \therefore k=\log_2n n=2kk=log2n,替换回去就得到上面表格 C w r o s t ( n ) = ⌊ log ⁡ 2 n ⌋ + 1 C_{wrost}(n)=\lfloor \log_2n \rfloor+1 Cwrost(n)=log2n+1,因为 n n n一定是整数,所以还得有一个向下取整的过程 ⌊ k ⌋ \lfloor k \rfloor k
  • 折半查找平均情况的效率仅仅比最差情况有轻微的改善。但是就键值依赖比较操作的查找算法来说,折半查找已经是一种最优的查找了(具有更优平均查找效率的查找算法:插值查找、散列法(甚至不需要有序))。
#include <stdio.h>
#include <stdlib.h>
#include <iostream> 
#include <time.h>
using namespace std;

void  DisplayArray(int *A, int length);
//递归实现
int BinarySearch_digui(int K,int *A,int from,int to){
	cout<<from<<" "<<to <<endl;
	if(from>=to){
		if(A[from]==K)
			return from;
		else return -1;
	}
	int m = from + (to-from)/2;
	if(A[m]==K){
		return m;
	}
	else{
		if(A[m]<K){
			int res_Qian = BinarySearch_digui(K,A,from,m-1);
			if(res_Qian!=-1){
				return res_Qian;
			}
		}
		else{
			int res_Hou = BinarySearch_digui(K,A,m+1,to);
			if(res_Hou!=-1){
				return res_Hou;
			}
		}
		return -1;		
	}
}

void  InsertionSort(int *A,int length){
	for(int i = 1; i<length; i++){
		bool IsInserted = false;
		int temp_change = A[i];
		for(int j = 0; j<i+1; j++){ //内层循环寻找当前待插入元素的合适位置 			
			if(A[i]>A[j] && !IsInserted){
				temp_change = A[j];
				A[j] = A[i];
				IsInserted = true;
			}
			else if(IsInserted){	
				int temp_change2 = A[j];
				A[j] = temp_change;
				temp_change = temp_change2;
			} 
		}			
	}
	cout<<"插入排序"<<endl;
	DisplayArray(A,length);
}

void  DisplayArray(int *A, int length){
	cout<<"数组打印:"<<endl;
	for(int i = 0; i<length; i++){
		cout<<A[i]<<" ";
	}
	cout<<endl;
} 

int main()
{
    srand(time(NULL));
    //printf ("%d\t",rand()%33);
    int length = rand()%30;
    while(length<=2){
		length = rand()%30;
	}
	int array[length];
	for(int i = 0; i<length; i++){
		array[i] = rand()%100;
	}
	DisplayArray(array,length);
	InsertionSort(array,length); //折半前提条件是有序数组,所以先将随机生成的数组进行插入排序
	int K = array[rand()%(length-1)];
	cout<<"随机生成的K为:"<<K<<endl; 
	int m = BinarySearch_digui(K,array,0,length-1);
	cout<<"折半查找键值为"<<K<<"的位置位于数组第"<<m+1<<"个元素"<<endl;
	
    return 0;
}
二分检索树
  • 定义:
    • 二分搜索树是一颗二叉树
    • 二分搜索树每个节点的左子树的值都小于该节点的值,每个节点右子树的值都大于该节点的值
    • 任意一个节点的每棵子树都满足二分搜索树的定义(即可以由递归得到)
      图片来自https://www.cnblogs.com/hello-shf/p/11342907.html
假币问题
  • 前提:不知道假币和真币谁轻谁重,唯一条件是只有一枚假币,其余都是真币。
  • 二分查找基本思想:将所有硬币分成两堆,每一堆有 ⌊ n 2 ⌋ \lfloor \frac{n}{2} \rfloor 2n个硬币。如果 n n n是奇数,则将最后一枚留下,剩下的均分为两堆,如果重量相同,则留下的这一枚是假币;如果重量不同,则再以同样的方式判断被均分后的两堆硬币(递归思想)。
    • 递推式: W ( n ) = W ( ⌊ n 2 ⌋ ) + 1 , W ( 1 ) = 0 W(n) =W(\lfloor \frac{n}{2} \rfloor)+1,W(1)=0 W(n)=W(2n)+1W(1)=0与最坏情况下的折半查找比较次数的递推式除了初始条件外基本一致。 原因:两种算法都是基于相同的设计技术:把问题规模减半
import java.util.*;
/**
 * 类 Coin 是假币问题的二分求解方法,基于减治法的减常因子法。
 * 采用的递归求解
 */
public class Coin
{   
    public static int []coins;
    private static Scanner in = new Scanner(System.in);
    
    public static void run() {
        System.out.println("-------------------------");
        System.out.print("请输入硬币的数目:");
        int n = in.nextInt(); //n Coins Number
        while(n<3){
            System.out.println("两枚及以下数量硬币无法判断真假,请重新输入硬币数量:");
            n = in.nextInt();
        }
        System.out.println("-------------------------");
        coins = new int[n];
        //n = (int) (Math.random() * n);
        Random random = new Random();
        int m = random.nextInt(n-1); //m表示假币位置
        int weight_T = random.nextInt(10);//生成真硬币重量
        while(weight_T<=0){
            weight_T = random.nextInt(10);
        }
        int weight_F = random.nextInt(10);//生成假硬币重量
        while(weight_F<=0 || weight_F==weight_T){
            weight_F = random.nextInt(10);
        }
        for(int i = 0; i<n; i++) coins[i] = weight_T;
        System.out.println("随机生成的假币重量为:" + weight_F);
        System.out.println("随机生成的真币重量为:" + weight_T);
        System.out.println("随机生成的假币位置为:" + m);
        coins[m] = weight_F;
        System.out.println("二分查找到的假币位置为:" + fun(0, coins.length-1) );
    }
    
    private static int fun(int from, int to) { //[form,to]
        if (to - from == 1) { //硬币数量为2
            if(coins[from]==coins[to])
                return -1;
            else{
                if(from==0){
                    if(coins[from]==coins[to+1]) return to;
                    else return from;
                }
                else{
                    if(coins[from]==coins[from-1]) return to;
                    else return from;
                }
            }
        }   
        int length = to - from + 1;
        if (length % 2 != 0) {
            length--;
        }
        int a = 0; //上半部分硬币重量
        for (int i = from; i < from + length / 2; i++) {
            a += coins[i];
        }
        int b = 0; //下半部分硬币重量
        for (int i = from + length / 2; i < from + length; i++) { //奇数:最后一个硬币没加上
            b += coins[i];
        }
        if (a == b && length != to - from && coins[to] != coins[to - 1]) { //奇数枚硬币,最后一枚是假的
            return to;
        }
        if (a != b) {           
                int res = fun(from, from + length / 2); 
                if (res != -1) {
                    return res;
                }                        
                int res1 = fun(from + length / 2, to);
                if (res1 != -1) {
                    return res1;
                }
                                   
        }
        return -1;
    }
}
俄氏乘法
  • 基本思想: n × m = n 2 × 2 m n\times m=\frac n2\times 2m n×m=2n×2m,对于 n n n 为奇数,则 n × m = n − 1 2 × 2 m + m n\times m=\frac {n-1}2\times 2m + m n×m=2n1×2m+m,直到 1 × m = m 1\times m=m 1×m=m,该算法使得硬件实现的速度非常快,仅仅使用二进制移位和基本的加法。
约瑟夫问题
  • J ( n ) J(n) J(n) 求解方法,将 n n n 化成二进制,进行一次向左循环移位即可得到结果。
  • 例如: J ( 6 ) = J ( 11 0 2 ) J(6)=J(110_2) J(6)=J(1102),想左循环移位得到 10 1 2 = 5 10 101_2=5_{10} 1012=510,化成十进制等于 5 5 5
减可变规模(没怎么出题)
欧几里得算法(辗转相除法)

第五章-分治法

分治法总述
  • 分治法基本思想
    • 最著名的通用算法技术
  • 求解步骤
    • 讲一个问题划分成同一类若干子问题(两个或多个),子问题最好规模相同
    • 将这些子问题求解(一般使用递归方法)但当问题规模足够小时,也可以直接求解
    • 有必要的话,合并这些子问题的解,以得到原始问题的答案。
  • 不是所有的分治法都一定比蛮力法更高效
  • 分治法对于并行运算是非常理想的,因为各个子问题都可以由各自的CPU同时计算。
  • 通用分治递推式:假设一个规模为n的实例可以被划分为b个规模为 n b \frac{n}{b} bn的实例,其中a个实例需要求解。为了简化分析,假设n是b的幂,对于算法的运行时间有如下递推式: T ( n ) = a T ( n b ) + f ( n ) T(n)=aT(\frac{n}{b})+f(n) T(n)=aT(bn)+f(n),其中 f ( n ) f(n) f(n)是一个函数,表示将问题分解为小问题和将结果合并起来所消耗的时间。
    • 应用主定理可以大大简化以上公式的计算:
    • 如果在递推式中== f ( n ) ϵ Θ ( n d ) f(n)\epsilon\Theta(n^d) f(n)ϵΘ(nd)==,其中 d ≥ 0 d\geq0 d0,那么会有
    • T ( n ) ϵ { Θ ( n d ) , 当 a < b d 时 Θ ( n d log ⁡ n ) , 当 a = b d 时 Θ ( n log ⁡ b a ) , 当 a > b d 时 T(n)\epsilon \begin{cases} \Theta(n^d),当a<b^d时\\ \Theta(n^d\log n),当a=b^d时\\ \Theta(n^{\log_ba}),当a>b^d时 \end{cases} T(n)ϵΘ(nd)a<bdΘ(ndlogn)a=bdΘ(nlogba)a>bd
    • 有了该结论,无需再对递推式求解,只要列出递推式就可以知道该算法的效率
    • 结论对于其他两个渐进符号也是成立的。
    • 难点就在于怎么求 f ( n ) f(n) f(n)
归并排序
思路
  • 对于一个需要排序的数组 A [ 0.. n − 1 ] A[0..n-1] A[0..n1],合并排序把它一分为二: A [ 0.. ⌊ n 2 ⌋ − 1 ] A[0..\lfloor \frac{n}{2} \rfloor-1] A[0..2n1] A [ ⌊ n 2 ⌋ . . n − 1 ] A[\lfloor \frac{n}{2} \rfloor..n-1] A[2n..n1],并对每个子数组递归排序,然后把这两个排好序的子数组合并为一个有序数组。这是递归部分
  • 合并部分:初始情况下,两个指针分别指向两个待合并数组第一个元素。然后比较这两个元素的大小,将较小的元素添加到一个新创建的数组中。接着,被复制数组中的指针后移,指向该较小元素的后继元素。上述操作一直持续到两个数组重点一个被处理完为止,然后,在未处理完的数组中,剩下的元素将被复制到新数组的尾部。
时间复杂度
  • 本算法的效率与具体输入情况有关,输入数组的好坏会直接影响比较次数。
    • 设Merge算法中进行比较的时 l e n g t h 1 length1 length1 l e n g t h 2 length2 length2
    • 最好情况下每一轮比较次数为 l e n g t h 1 < l e n g t h 2 ? l e g n t h 1 : l e n g t h 2 length1<length2 ?legnth1:length2 length1<length2legnth1:length2 ,两个被划分到数组中长度较短的那一个。
    • 最坏情况下每一轮比较次数为 n − 1 n-1 n1,也就是 l e n g t h 1 + l e n g t h 2 − 1 length1+length2-1 length1+length21 次比较。
  • 根据分治法的通用递推公式和主定理可以快速求解出该排序算法的时间效率。
  • 本算法基本操作是合并中的键值不断进行比较,所以 C m e r g e ( n ) C_{merge}(n) Cmerge(n)求解的是合并阶段进行键值比较的次数。
    • 最坏情况下:例如 B [ 4 ] = B[4]= B[4]= {1,3,5,7}, C [ 4 ] = C[4]= C[4]={2,4,6,8},需要比较7次。所以可以得到最坏情况下n个元素分成两组进行比较次数为 n − 1 n-1 n1 次。记住这 C m e r g e ( n ) C_{merge}(n) Cmerge(n)仅为一轮递归中的比较次数,而非完整一个归并排序中所有的比较次数之和。
    • n > 1 n>1 n>1时, C ( n ) = 2 C ( n 2 ) + C m e r g e ( n ) , C ( 1 ) = 0 C(n)=2C(\frac{n}{2})+C_{merge}(n),C(1)=0 C(n)=2C(2n)+Cmerge(n)C(1)=0
    • 最坏情况下: C m e r g e ( n ) = n − 1 C_{merge}(n)=n-1 Cmerge(n)=n1
  • 由主定理可知, a = 2 , b = 2 , d = 1 , ∴ a = b d , C w o r s t ϵ Θ ( n d log ⁡ n ) a=2,b=2,d=1,\therefore a=b^d,C_{worst}\epsilon\Theta(n^d\log n) a=2b=2d=1a=bdCworstϵΘ(ndlogn) C ( n ) ϵ Θ ( n log ⁡ n ) C(n)\epsilon\Theta(n\log n) C(n)ϵΘ(nlogn)
实现代码
#include <stdio.h>
#include <stdlib.h>
#include <iostream> 
#include <time.h>
using namespace std;

void  DisplayArray(int *A, int length,int from,int to);
void  Merge(int *A,int from1,int to1,int from2,int to2); 

/*
*归并排序
*算法思路:分为递归部分与合并部分,递归部分不断递归调用,合并部分实现排序并合并 
*/
void MergeSort(int *A,int length,int from,int to){ //递归部分
	cout<<from<<" "<<to<<" Length: "<<length<<endl;
	if(from-to<=-1){
		int m = from + length/2;
		MergeSort(A,length/2,from,m-1);
		MergeSort(A,to-m+1,m,to);
		Merge(A,from,m-1,m,to);
	}
} 

void Merge(int *A,int from1,int to1,int from2,int to2){ //合并/排序部分
	cout<<from1<<" "<<to1<<"   "<<from2<<" "<<to2<<endl;
	if(from2-to1==1){
		int B[to1-from1+1];
		int C[to2-from2+1];
		int len1 = to1-from1+1;
		int len2 = to2-from2+1;
		for(int i = 0,j = 0; i<len1 || j<len2; i++,j++){
			if(i<len1){
				 B[i] = A[from1+i];
				 cout<<B[i]<<endl;
			}
			if(j<len2) {
				C[j] = A[from2+j];
				cout<<C[j]<<endl;
			}
		}
		int i = 0,j = 0;
		while(i<len1&&j<len2){
			if(B[i]>C[j]){
				A[from1+i+j] = B[i];
				i++;
			}
			else{
				A[from1+i+j] = C[j];
				j++;
			}			
		}
		if(i==len1){
			while(j<len2){
				A[from1+i+j]=C[j];
				j++;
			}
		}
		else if(j==len2){
			while(i<len1){
				A[from1+i+j] = B[i];
				i++;
			}
		}
		DisplayArray(A,to2-from1+1,from1,to2);
	}
}

void  DisplayArray(int *A, int length,int from,int to){
	cout<<"数组打印:"<<endl;
	for(int i = from; i<=to; i++){
		cout<<A[i]<<" ";
	}
	cout<<endl;
} 

int main()
{
    srand(time(NULL));
    //printf ("%d\t",rand()%33);
    int length = rand()%30;
    while(length<=2){
		length = rand()%30;
	}
	int array[length];
	for(int i = 0; i<length; i++){
		array[i] = rand()%100;
	}
	DisplayArray(array,length,0,length-1);
	MergeSort(array,length,0,length-1); 
	DisplayArray(array,length,0,length-1);
	
    return 0;
}
键的比较次数基本操作执行次数时间复杂度空间复杂度稳定性在位性
最坏情况 C ( n ) = 2 C ( n 2 ) + ( n − 1 ) , C ( 1 ) = 0 C(n)=2C(\frac{n}{2})+(n-1),C(1)=0 C(n)=2C(2n)+(n1)C(1)=0Θ( n log ⁡ n n\log n nlogn)额外辅助空间常数个稳定不在位
平均情况Θ( n log ⁡ n n\log n nlogn)额外辅助空间常数个稳定不在位
归并排序总结
  • 相较于两个高级排序算法(堆排序与快速排序),归并排序的显著优点在于其稳定性,是一个稳定排序算法。
  • 缺点:需要线性额外空间,虽然可以通过改进使其变成在位算法,但是算法实现过程复杂,从而只在理论上有效。
快速排序
思路
  • 归并排序基于元素在数组中的位置进行分治(划分),而快速排序基于元素值大小进行分治(划分)。
  • 快速排序的划分,使得 A [ s ] A[s] A[s]左边的元素都大于/小于 A [ s ] A[s] A[s],右边的元素都小于/大于 A [ s ] A[s] A[s]。建立此划分即得到了 A [ s ] A[s] A[s]在整个数组中的最终位置,然后继续对 A [ s ] A[s] A[s]左边和右边的两组划分进行分治(递归思想)。
  • 快速排序中,算法的主要工作在于划分阶段,不需要再去合并子问题的解了。
  • 划分阶段采用的算法是霍尔算法:
    • 霍尔算法将当前数组 A [ f r o m , t o ] A[from,to] A[fromto] 的第一个元素作为划分元素 m = A [ f r o m ] m = A[from] m=A[from]
    • 两个指针 i , j i,j ij,一个指向当前数组第二个元素,另一个指针指向当前数组最后一个元素,相向进行扫描。
    • i i i 指向的元素大于/小于划分元素 m m m 且(&&) j j j 指向的元素小于/大于划分元素 m m m 时(注意:两个指针的移动不一定是同步的,有可能某一个指针指向大于/小于元素 m m m 的值,但是另一个指针还未指向小于/大于元素 m m m ,这时需要继续移动另一个,直到两个指针都满足条件),交换两个指针指向的元素 (注意:不是交换指针)
    • i ≥ j i\geq j ij时,本轮以 m = A [ f r o m ] m = A[from] m=A[from]为划分元素的划分结束
    • 此时 j j j 指针指向的位置就是划分元素 m m m 的最终位置,交换 ∗ j *j j m m m,即完成。
  • 再对两个被 m m m 划分的数组部分进行分治(划分)。
时间复杂度
  • 快速排序中也是通过比较次数来衡量。
  • 快速排序中比较次数与具体输入情况有关。
    • 最优情况,是每一轮划分元素的位置处于当前数组 A [ f r o m , t o ] A[from,to] A[fromto] 的中间。这样仅需要后面每一个元素与划分元素比较一次,由于最后 i ≥ j i\geq j ij ,肯还会多出 1~2 比较,所以比较次数 ϵ Θ ( n ) \epsilon\Theta(n) ϵΘ(n),即 d = 1 d=1 d=1
    • 最坏情况,是每一轮划分元素的位置处于当前数组 A [ f r o m , t o ] A[from,to] A[fromto] 的某一端。例如一个非降排序,则初始用第一个(也就是整个数组最小元素)作为划分元素,得到划分结果是其中一个是空集,另一个是剩下所有元素。最坏情况下的快速排序退化成了选择排序
  • 递推公式同样可以套用分治法通用递推公式,只需要找到 f ( n ) f(n) f(n) ,其中 n = t o − f r o m + 1 n=to-from+1 n=tofrom+1表示当前数组长度。
    • 最优情况下的递推式,每一轮比较次数为 n n n,按照通用递推式写出快速排序最优情况下的: C b e s t ( n ) = 2 C b e s t ( n 2 ) + n , C b e s t ( 1 ) = 0 C_{best}(n)=2C_{best}(\frac{n}{2})+n,C_{best}(1)=0 Cbest(n)=2Cbest(2n)+nCbest(1)=0
    • 最坏情况下退化成选择排序的最坏情况,所以 C w o r s t = ( n + 1 ) + n + … … + 3 = ( n + 1 ) ( n + 2 ) 2 − 3 ϵ Θ ( n 2 ) C_{worst}=(n+1)+n+……+3=\frac{(n+1)(n+2)}{2}-3\epsilon\Theta(n^2) Cworst=(n+1)+n++3=2(n+1)(n+2)3ϵΘ(n2)
    • 平均情况下,假设分裂点最终位于每个位置的概率都是 1 n \frac{1}{n} n1 ,递推式为 1 n ∑ s = 0 n − 1 [ ( n + 1 ) + C a v g ( s ) + C a v g ( n − 1 − s ) ] , C a v g ( 0 ) = 0 , C a v g ( 1 ) = 1 \frac{1}{n}\sum_{s=0}^{n-1}[(n+1)+C_{avg}(s)+C_{avg}(n-1-s)],C_{avg}(0)=0,C_{avg}(1)=1 n1s=0n1[(n+1)+Cavg(s)+Cavg(n1s)]Cavg(0)=0Cavg(1)=1,结果为 C a v g ≈ 2 n ln ⁡ n ≈ 1.39 n log ⁡ 2 n C_{avg}\approx2n\ln n\approx 1.39n\log_2n Cavg2nlnn1.39nlog2n
快速排序代码
快速排序总结
  • 快速排序在平均情况下,仅比最优情况多执行 39 % 39\% 39% 的比较操作
  • 其内层循环效率非常高,在处理随机排列的数组时,速度比合并排序(归并排序)快
  • 缺点:不稳定,还需要一个栈存储未被排序的子数组参数??(相较于堆排序空间效率 O ( 1 ) O(1) O(1)比较差)。
大整数乘法与Strassen矩阵乘法
大整数乘法
  • 对于超过100位的十进制整数进行乘法运算,由于整数长度过长,计算机的“字”长不够表示,所以CPU运算器不能直接进行运算。
  • 按照列出乘法竖式手算的逻辑,乘数的每一位都要与被乘数进行一次 n n n 位和 1 1 1 位的乘法运算,这样一共需要 n ∗ m n*m nm 次位乘。
Strassen(斯特拉森)矩阵乘法
  • 传统的矩阵相乘,所需要的惩罚次数 n 3 n^3 n3,加法次数 n 3 n^3 n3 n 3 − n 2 n^3-n^2 n3n2
  • 而斯特拉森算法计算 2 ∗ 2 2*2 22 矩阵只需要进行7次乘法,这里用 M ( n ) M(n) M(n) 表示乘法次数,用 A ( n ) A(n) A(n) 表示加法次数,得到递推公式:
    • 乘法次数: M ( n ) = 7 ∗ M ( n 2 ) , M ( 1 ) = 1 M(n)=7*M(\frac n2),M(1)=1 M(n)=7M(2n)M(1)=1,得到 a = 7 , b = 2 , d = 0 a=7,b=2,d=0 a=7,b=2,d=0,根据大定理,满足 a > b d ∴ M ( n ) ϵ Θ ( n l o g 2 7 ) = Θ ( n 2.81 ) a>b^d \therefore M(n)\epsilon\Theta(n^{log_27})=\Theta(n^{2.81}) a>bdM(n)ϵΘ(nlog27)=Θ(n2.81)
    • 加法次数: A ( n ) = 7 ∗ A ( n 2 ) + 18 ( n 2 × n 2 ) , A ( 1 ) = 0 A(n)=7*A(\frac n2)+18(\frac n2\times\frac n2),A(1)=0 A(n)=7A(2n)+18(2n×2n)A(1)=0,的都 a = 7 , b = 2 , d = 2 a=7,b=2,d=2 a=7,b=2,d=2,根据大定理,满足 a > b d ∴ A ( n ) ϵ Θ ( n l o g 2 7 ) = Θ ( n 2.81 ) a>b^d\therefore A(n)\epsilon\Theta(n^{log_27})=\Theta(n^{2.81}) a>bdA(n)ϵΘ(nlog27)=Θ(n2.81)
  • 由于加法次数与乘法次数是相同的,所以Strassen斯特拉森矩阵乘法属于 Θ ( n 2.81 ) \Theta(n^{2.81}) Θ(n2.81)
  • 当计算的矩阵规模很小时,该算法的优势体现不出来,但是它的重要性体现在当矩阵的阶趋于无穷大时,该算法表现出来的卓越的渐近效率
  • 矩阵乘法在理论上的效率下界是 n 2 n^2 n2
#include <iostream>
#include <iomanip>
using namespace std;

int ** juzhenAdd(int size,int **A,int **B){ //矩阵相加 
	int **C = new int*[size];
	for(int i = 0; i<size; i++){
		C[i] = new int[size];
	}
	for(int i = 0; i<size; i++){
		for(int j = 0; j<size; j++){
			C[i][j] = A[i][j] + B[i][j];
		}
	}
	return C;	
}

int ** juzhenMinus(int size,int **A,int **B){ //矩阵相减 
	int **C = new int*[size];
	for(int i = 0; i<size; i++){
		C[i] = new int[size];
	}
	for(int i = 0; i<size; i++){
		for(int j = 0; j<size; j++){
			//cout<<A[i][j]<<endl;
			//cout<<B[i][j]<<endl; 
			C[i][j] = A[i][j] - B[i][j];
		}
	}
	return C;
}

int ** juzhen11(int size,int **X){  
	int **C = new int*[size/2];
	for(int i = 0; i<size/2; i++){
		C[i] = new int[size/2];
	}
	for(int i = 0; i<size/2; i++){
		for(int j = 0; j<size/2; j++){
			C[i][j] = X[i][j];
		}
	}
	return C;
}

int ** juzhen12(int size,int **X){
	int size_new = size/2;
	int **C = new int*[size_new];
	for(int i = 0; i<size_new; i++){
		C[i] = new int[size_new];
	}
	for(int i = 0; i<size_new; i++){
		for(int j = 0; j<size_new; j++){
			C[i][j] = X[i][j+size_new];
		}
	}
	return C;
}

int ** juzhen21(int size,int **X){
	int size_new = size/2;
	int **C = new int*[size_new];
	for(int i = 0; i<size_new; i++){
		C[i] = new int[size_new];
	}
	for(int i = 0; i<size_new; i++){
		for(int j = 0; j<size_new; j++){
			C[i][j] = X[i+size_new][j];
		}
	}
	return C;
}

int ** juzhen22(int size,int **X){
	int size_new = size/2;
	int **C = new int*[size_new];
	for(int i = 0; i<size_new; i++){
		C[i] = new int[size_new];
	}
	for(int i = 0; i<size_new; i++){
		for(int j = 0; j<size_new; j++){
			C[i][j] = X[i+size_new][j+size_new];
		}
	}
	return C;
}

int ** XiangCheng(int size, int **A, int **B){ ///矩阵相乘递归 
	int size_new = size/2;
	if(size_new==1){
		int **C = new int*[size];
		for(int i = 0; i<size; i++){
			C[i] = new int[size];
		}
		int M1 = (A[0][0] + A[1][1]) * (B[0][0] + B[1][1]);
		int M2 = (A[1][0] + A[1][1]) * B[0][0];
		int M3 = A[0][0] * (B[0][1] - B[1][1]);
		int M4 = A[1][1] * (B[1][0] - B[0][0]);
		int M5 = (A[0][0] + A[0][1]) * B[1][1];
		int M6 = (A[1][0] - A[0][0]) * (B[0][0] + B[0][1]);
		int M7 = (A[0][1] - A[1][1]) * (B[1][0] + B[1][1]);
		
		C[0][0] = M1 + M4 - M5 + M7;
		C[0][1] = M3 + M5;
		C[1][0] = M2 + M4;
		C[1][1] = M1 - M2 + M3 + M6;
		
		return C;
	}
	//A11
	int **A11 = new int*[size_new];
	for(int i = 0; i<size_new; i++){
		A11[i] = new int[size_new];
	}
	A11 = juzhen11(size,A);
	//A12
	int **A12 = new int*[size_new];
	for(int i = 0; i<size_new; i++){
		A12[i] = new int[size_new];
	}
	A12 = juzhen12(size,A);
	
	//A21
	int **A21 = new int*[size_new];
	for(int i = 0; i<size_new; i++){
		A21[i] = new int[size_new];
	}
	A21 = juzhen21(size,A);
	//A22
	int **A22 = new int*[size_new];
	for(int i = 0; i<size_new; i++){
		A22[i] = new int[size_new];
	}
	A22 = juzhen22(size,A);
	//B11
	int **B11 = new int*[size_new];
	for(int i = 0; i<size_new; i++){
		B11[i] = new int[size_new];
	}
	B11 = juzhen11(size,B);
	//B12
	int **B12 = new int*[size_new];
	for(int i = 0; i<size_new; i++){
		B12[i] = new int[size_new];
	}
	B12 = juzhen12(size,B);
	//B21
	int **B21 = new int*[size_new];
	for(int i = 0; i<size_new; i++){
		B21[i] = new int[size_new];
	}
	B21 = juzhen21(size,B);
	//B22
	int **B22 = new int*[size_new];
	for(int i = 0; i<size_new; i++){
		B22[i] = new int[size_new];
	}
	B22 = juzhen22(size,B);
	
	//S1 = B12 - B22
	int **S1 = new int*[size_new];
	for(int i = 0; i<size_new; i++){
		S1[i] = new int[size_new];
	}
	S1 = juzhenMinus(size_new,B12,B22);
	//S2 = A11 + A12
	int **S2 = new int*[size_new];
	for(int i = 0; i<size_new; i++){
		S2[i] = new int[size_new];
	}
	S2 = juzhenAdd(size_new,A11,A12);
	//S3 = A21 + A22
	int **S3 = new int*[size_new];
	for(int i = 0; i<size_new; i++){
		S3[i] = new int[size_new];
	}
	S3 = juzhenAdd(size_new,A21,A22);
	//S4 = B21 - B11
	int **S4 = new int*[size_new];
	for(int i = 0; i<size_new; i++){
		S4[i] = new int[size_new];
	}
	S4 = juzhenMinus(size_new,B21,B11);
	//S5 = A11 + A22
	int **S5 = new int*[size_new];
	for(int i = 0; i<size_new; i++){
		S5[i] = new int[size_new];
	}
	S5 = juzhenAdd(size_new,A11,A22);
	//S6 = B11 + B22
	int **S6 = new int*[size_new];
	for(int i = 0; i<size_new; i++){
		S6[i] = new int[size_new];
	}
	S6 = juzhenAdd(size_new,B11,B22);
	//S7 = A12 - A22
	int **S7 = new int*[size_new];
	for(int i = 0; i<size_new; i++){
		S7[i] = new int[size_new];
	}
	S7 = juzhenMinus(size_new,A12,A22);
	//S8 = B21 + B22
	int **S8 = new int*[size_new];
	for(int i = 0; i<size_new; i++){
		S8[i] = new int[size_new];
	}
	S8 = juzhenAdd(size_new,B21,B22);
	//S9 = A11 - A21
	int **S9 = new int*[size_new];
	for(int i = 0; i<size_new; i++){
		S9[i] = new int[size_new];
	}
	S9 = juzhenMinus(size_new,A11,A21);
	//S10 = B11 + B12
	int **S10 = new int*[size_new];
	for(int i = 0; i<size_new; i++){
		S10[i] = new int[size_new];
	}
	S10 = juzhenAdd(size_new,B11,B12);
	
	//M1 
	int **M1 = new int*[size_new];
	for(int i = 0; i<size_new; i++){
		M1[i] = new int[size_new];
	}
	M1 = XiangCheng(size_new,A11,S1);
	//M2
	int **M2 = new int*[size_new];
	for(int i = 0; i<size_new; i++){
		M2[i] = new int[size_new];
	}
	M2 = XiangCheng(size_new,S2,B22);
	//M3
	int **M3 = new int*[size_new];
	for(int i = 0; i<size_new; i++){
		M3[i] = new int[size_new];
	}
	M3 = XiangCheng(size_new,S3,B11);
	//M4
	int **M4 = new int*[size_new];
	for(int i = 0; i<size_new; i++){
		M4[i] = new int[size_new];
	}
	M4 = XiangCheng(size_new,A22,S4);
	//M5
	int **M5 = new int*[size_new];
	for(int i = 0; i<size_new; i++){
		M5[i] = new int[size_new];
	}
	M5 = XiangCheng(size_new,S5,S6);
	//M6
	int **M6 = new int*[size_new];
	for(int i = 0; i<size_new; i++){
		M6[i] = new int[size_new];
	}
	M6 = XiangCheng(size_new,S7,S8);
	//M7
	int **M7 = new int*[size_new];
	for(int i = 0; i<size_new; i++){
		M7[i] = new int[size_new];
	}
	M7 = XiangCheng(size_new,S9,S10);

	//C11
	int **C11 = new int*[size_new];
	for(int i = 0; i<size_new; i++){
		C11[i] = new int[size_new];
	}
	C11 = juzhenAdd(size_new,juzhenMinus(size_new,juzhenAdd(size_new,M5,M4),M2),M6);
	//C12
	int **C12 = new int*[size_new];
	for(int i = 0; i<size_new; i++){
		C12[i] = new int[size_new];
	}
	C12 = juzhenAdd(size_new,M1,M2);
	//C21
	int **C21 = new int*[size_new];
	for(int i = 0; i<size_new; i++){
		C21[i] = new int[size_new];
	}
	C21 = juzhenAdd(size_new,M3,M4);
	//C22
	int **C22 = new int*[size_new];
	for(int i = 0; i<size_new; i++){
		C22[i] = new int[size_new];
	}
	C22 = juzhenMinus(size_new,juzhenMinus(size_new,juzhenAdd(size_new,M5,M1),M3),M7);

	//由C11,C12,C21,C22合并成为矩阵C
	int **C = new int*[size];
	for(int i = 0; i<size; i++){
		C[i] = new int[size];
	}
	for(int i = 0; i<size; i++){
		for(int j = 0; j<size; j++){
			if(i<size_new && j<size_new){
				C[i][j] = C11[i][j];
			}
			else if(i<size_new && j>=size_new){
				C[i][j] = C12[i][j-size_new];
			}
			else if(i>=size_new && j<size_new){
				C[i][j] = C21[i-size_new][j];
			}
			else{
				C[i][j] = C22[i-size_new][j-size_new];
			}
		}
	}
	return C;
}

int main(){
	int size = 0;
	do{
		cout<<"请输入矩阵SIZE(须是2的幂):";
		cin>>size;	
	}while(size & (size-1) != 0);
	//输入矩阵A 
	int **A = new int*[size];
	for(int i = 0; i<size; i++){
		A[i] = new int[size];
	}
	cout<<"请输入矩阵A:"<<endl;
	for(int i = 0; i<size; i++){
		for(int j = 0; j<size; j++){
			cin>>A[i][j];
		}
	}
	//输入矩阵B
	int **B = new int*[size];
	for(int i = 0; i<size; i++){
		B[i] = new int[size];
	}
	cout<<"请输入矩阵B:"<<endl;
	for(int i = 0; i<size; i++){
		for(int j = 0; j<size; j++){
			cin>>B[i][j];
		}
	}
	//申请结果矩阵C,调用相加函数
	int **C = new int*[size];
	for(int i = 0; i<size; i++){
		C[i] = new int[size];
	}
	C = XiangCheng(size,A,B);
	//结果输出 
	for(int i = 0; i<size; i++){
		for(int j = 0; j<size; j++){
			cout<<setiosflags(ios::left)<<setw(7)<<C[i][j];
		}
		cout<<endl;
	}
	return 0;
}

/*
测试数据:
1 2 3 4
4 3 2 1
5 6 7 8
8 7 6 5
*/ 

第六章-变治法(考试这一章考得比较简单)

变治法总述
  • 的阶段把问题的实例变得更容易求解,的阶段对变换后的实例进行求解。
  • 的思想有三种
    • 实例化简——变换为同样问题的一个更简单或者更方便的实例(高斯消去法);
    • 改变表现——变换为同样实例的不同表现(堆排序,霍纳法则);
    • 问题化简——变换为另一个问题的实例(求最小公倍数,优化问题的化简)。
高斯消去法
  • 掌握手动计算,高斯消去
  • 基本思路:把 n n n 个线性方程组构成的 n n n 元联立方程组变换为一个等价的方程组,该方程组有着一个上三角形矩阵,这种矩阵对角线以下的元素全部为0。而以上变换可以通过一系列初等变换来做到这一点。
    • 交换方程组中两个方程的位置。
    • 把一个方程替换成另一个方程。
    • 把一个方程替换为它和另一个倍数之间的关系。
  • 所有的初等变换都不会改变方程组的解
  • 详见教材P162
堆排序
  • 判断是不是一个堆
  • 堆可以定义为一棵完全二叉树,这颗二叉树必须满足以下两个条件:
    • 树的形状,必须是一棵完全二叉树。这意味着树的每一层都是满的,除了最后一层最右边的元素有可能缺位。
    • 父母优势每一个节点的键(每个节点包含一个键)都要大于或等于它子女的键。
  • 堆的特性:
    • 只存在一棵 n n n 跟节点的完全二叉树,高度为 ⌊ log ⁡ 2 n ⌋ \lfloor{\log_2n}\rfloor log2n
    • 堆的根总是包含了堆的最大元素
    • 堆的最大节点以及该节点的子孙也是一个堆。
    • 可以用数组来实现堆,方法是从上到下、从左到右的方式来记录堆的元素
  • 堆排序的步骤(自低向上
    • 构造堆,即为一个给定的数组构造一个堆, O ( n ) \Omicron (n) O(n)
    • 删除最大堆,即对剩下的堆应用 n − 1 n-1 n1 次根删除操作。 O ( log ⁡ n ) \Omicron (\log n) O(logn)
  • 堆排序的步骤(自顶向下
    • 构造堆 O ( n log ⁡ n ) \Omicron (n\log n) O(nlogn)
package algorithm;

import java.util.Random;
import java.util.Scanner;

/**
 * 4. 变治法在排序问题中的应用——堆排序问题
 */
public class Work4 {
    private static Scanner input = new Scanner(System.in);

    private static void HeapBottomUp(int[] A, int length) {
        int k = 0;
        int v = 0;
        int j = 0;
        for (int i = length / 2; i >= 0; i--) {
            k = i;
            v = A[k];
            boolean heap = false;
            while (!heap && 2 * k <= length) {
                j = 2 * k;
                if (j < length) {
                    if (A[j] < A[j + 1]) j = j + 1;
                }
                if (v >= A[j]) {
                    heap = true;
                } else {
                    A[k] = A[j];
                    k = j;
                }
            }
            A[k] = v;
        }
    }

    private static int Sort(int[] A, int length) {
        int k;
        if (length >= 1) {
            HeapBottomUp(A, length);
            // 交换
            int temp = A[0];
            A[0] = A[length];
            A[length] = temp;
            return Sort(A, length - 1);
        }
        for (int i = 0; i < A.length - 1; i++) {
            A[i] = A[i + 1];
        }
        return -1;
    }

    public static void run() {
        System.out.println("-------------------------");
        System.out.print("输入待排序的数组元素个数:");
        int length = input.nextInt();
        int[] A = new int[length + 1];//初始化数组
        System.out.println("0.随机生成数字");
        System.out.println("1.手动输入数字");
        System.out.print("请选择:");
        int mode = input.nextInt();
        if (mode == 1) {
            System.out.println("请输入" + length + "个数字:");
            for (int i = 0; i < length; i++) {
                A[i] = input.nextInt();
            }
        } else if (mode == 0) {
            Random random = new Random();
            for (int i = 0; i < length; i++) {
                A[i] = random.nextInt(1000);
            }
            System.out.print("随机生成的数为:");
            for (int i = 0; i < length; i++) {
                System.out.print(A[i] + " ");
            }

            System.out.println();
        }
        long time = System.currentTimeMillis();
        Work4.Sort(A, length);
        time = System.currentTimeMillis() - time;
        for (int i = 0; i < A.length - 1; i++) {
            System.out.print(A[i] + " ");
        }
        System.out.println();
        System.out.println("堆排序所用时间为:" + time + "ms");
    }
}

霍纳法则(必考)
  • 霍纳法则是一个古老的计算多项式的算法,思路很简单清楚
  • 例如计算多项式 p ( x ) = 2 x 4 − x 3 − 3 x 2 + x − 5 p(x)=2x^4-x^3-3x^2+x-5 p(x)=2x4x33x2+x5,其中 x = 3 x=3 x=3 时的值
    • 计算表格: = x × 上 一 格 的 值 + 当 前 系 数 =x\times 上一格的值 + 当前系数 =x×+
    • 思路是只需要用一个表格来记录多项式每一项的系数,得到 p ( 3 ) = 160 p(3)=160 p(3)=160
    • 注意计算顺序:高次系数 → \rightarrow 低次系数
系数2-131-5
x = 3 x=3 x=3 2 2 2 3 × 2 + ( − 1 ) = 5 3\times2+(-1)=5 3×2+(1)=5 5 × 3 + 3 = 18 5\times3+3=18 5×3+3=18 18 × 3 + 1 = 55 18\times3+1=55 18×3+1=55 55 × 3 + ( − 5 ) = 160 55\times3+(-5)=160 55×3+(5)=160
  • 霍纳法则的加法次数与乘法次数均为 n n n 次,算法效率 ϵ Θ ( n ) \epsilon\Theta(n) ϵΘ(n)
//系数数组存储的系数是按照 高位到低位,即[2,-1,3,1,-5]
//                                  [x^4 ~ x^0]
int Horner(int *A,n,x){ //n表示这是一个n次多项式,x表示代入计算的值
	int result = A[0];
	for(int i = 1; i<=n; i++){
		result = result*x+A[i];
	}
	return result;
}
问题化简
求最小公倍数(lcm,least common multiple)
  • 基本思路:将利用质因数分解求解最小公倍数转换成先利用欧几里得算法计算最大公约数 g c d ( m , n ) gcd(m,n) gcd(m,n),再利用公式 l c m ( m , n ) = m × n g c d ( m , n ) lcm(m,n)=\frac{m\times n}{gcd(m,n)} lcm(m,n)=gcd(m,n)m×n
  • 欧几里得算法(辗转相除法)求解最大公约数
    • 将每次除得余数作第二次的除数,除数作第二次的被除数,直到余数为0,本次除数(上一次余数)即为最大公约数。
    • 代码如下:
/*
辗转相除法基本思想:
被除数 / 除数 =商...余数
将每次除得余数作第二次的除数,除数作第二次的被除数,
直到余数为0,本次除数(上一次余数)即为最大公约数。
*/
int lcm_ab = 0;
while(1){
	  cout<<a<<"/"<<b<<"="<<a/b<<"..."<<a%b<<endl;
	  lcm_ab = a%b;
	  a = b;
	  b = lcm_ab;
	  if(a%b==0) break;
} 
优化问题的化简
  • 基本思路:(求某一个函数的最大值的问题将其称为最大化问题,同理也有最小化问题),如果已知求函数最大值的算法,可以利用公式 m i n f ( x ) = − m a x [ − f ( x ) ] minf(x)=-max[-f(x)] minf(x)=max[f(x)] ,即求最小值可以转换成求其负函数(即与 x x x 轴对称的函数)的最大值
  • 微积分过程不是算法,因为不是每一个微积分都能得到结果。
习题第2题
  • 给定一个数字的列表,我们需要为它构造一个最小堆(最小堆是一 棵完全二叉树。其中的每个键都小于等于它子女中的键)。我们如何利用构造最大堆(在6.4节中定义的堆)的算法来构造最小堆?
  • 答:利用优化问题的化简思路,将所有键值 × ( − 1 ) \times(-1) ×1 再构造最大堆即可。

第八章-动态规划

动态规划总述
  • 也称表格计算(tabler compute)
  • 两个特点
    • 最优子结构
    • 重叠子问题(与分治法的区别:动态规划中的子问题之间存在关联,是相关的,而分治法子问题是各自独立求解
  • 动态规划求解四个步骤
    • 最优解的形式(集合?数组?……)
    • 衡量是否是最优解的条件(例如:币值最大化问题中的硬币面值大小是衡量是否是最优解的度量)
    • 自底向上计算衡量条件
    • (回溯)构造解决办法
背包问题
  • (考试要求:要会填表,会构造递推式)
  • 问题描述:给定 n n n 个重量为 w 1 , w 2 , … , w n w_1,w_2,…,w_n w1,w2,,wn,价值为 v 1 , … , v n v_1,…,v_n v1vn 的物品和一个承受能力为 W W W 的背包,求怎样选择物品放进背包,让背包中的价值最大,且不超重。(假设物品重量和背包承重为整数,而物品数量不一定是整数)
  • 最优解的形式:二元组 A = ( { 1 , 2 , … , n } , W ) = { n ∈ A n ∉ A A=(\{1,2,…,n\},W)=\begin{cases} n\in A \\ n \notin A \end{cases} A=({1,2,,n}W)={nAn/A ,所以构建一个 n × W n\times W n×W的表格(矩阵)
  • 得到递推式, i i i 表示第 i i i 个物品, j j j 表示背包容量大小。
  • F ( i , j ) = { m a x { F ( i − 1 , j ) , F ( i − 1 , j − w i ) + v i } , j − w i ≥ 0 F ( i − 1 , j )                                                   , j − w i < 0 F(i,j)=\begin{cases}max\{F(i-1,j),F(i-1,j-w_i)+v_i\},j-w_i\geq0\\F(i-1,j)\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ ,j-w_i<0 \end{cases} F(i,j)={max{F(i1,j),F(i1,jwi)+vi},jwi0F(i1,j)                                                 ,jwi<0
  • 递推式初始条件: F ( 0 , j ) = 0 , F ( i , 0 ) = 0 F(0,j)=0,F(i,0)=0 F(0,j)=0F(i,0)=0,即表格(矩阵)的第一行第一列全为0。
物品重量价值
1 2 2 2 12 12 12
2 1 1 1 10 10 10
3 3 3 3 20 20 20
4 2 2 2 15 15 15
j i j\\i ji012345
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
1 0 0 0 0 0 0 12 12 12 12 12 12 12 12 12 12 12 12
2 0 0 0 10 10 10 12 12 12 22 22 22 22 22 22 22 22 22
3 0 0 0 10 10 10 12 12 12 22 22 22 30 30 30 32 32 32
4 0 0 0 10 10 10 15 15 15 25 25 25 30 30 30 37 37 37
  • 回溯过程:比较 F ( i , j ) 和 F ( i − 1 , j ) F(i,j)和F(i-1,j) F(i,j)F(i1,j)

    • 不相等一定最终结果一定包含第 i i i 个物品,相等则还需要比较 F ( i − 1 , j )   和   F ( i − 1 , j − w i ) + v i F(i-1,j)\ 和\ F(i-1,j-w_i)+v_i F(i1,j)  F(i1,jwi)+vi
      • 如果 F ( i − 1 , j )   >   F ( i − 1 , j − w i ) + v i F(i-1,j)\ >\ F(i-1,j-w_i)+v_i F(i1,j) > F(i1,jwi)+vi,则包含第 i − 1 i-1 i1 个物品。
      • 如果相等则存在这样的可能:第 i i i 个物品存在的价值总和与第 i i i 个物品不存在但是第 i − 1 i-1 i1 个物品存在的价值总和相等,那么则出现了有多种不同的最优子集。
  • 对于背包问题的效率分析

    • 填表阶段: { 时 间 效 率 ∈ Θ ( n W ) 空 间 效 率 ∈ Θ ( n W ) \begin{cases} 时间效率\in \Theta(nW) \\ 空间效率 \in \Theta(nW) \end{cases} {Θ(nW)Θ(nW)
    • 回溯求最优子集组成元素的过程, 时 间 效 率 ∈ O ( n ) 时间效率\in \Omicron(n) O(n) O \Omicron O小于等于
Warshell算法与Floyd算法
Warshell算法
  • (考试要求:Warshell算法要会填表)
  • 基本思路: n n n 个节点的有向图( n n n 阶邻接矩阵),求解传递闭包。循环遍历第 i i i 列,让第 i i i 列为 1 1 1 的那一行 x x x 与第 i i i 行进行并运算,改变第 x x x 行的值。 i++ 直到i<n
  • 算法效率: ∈ Θ ( n 3 ) \in \Theta(n^3) Θ(n3)
Floyd算法
  • 时间效率: ∈ Θ ( n 3 ) \in \Theta(n^3) Θ(n3)

第九章-贪婪技术

贪婪技术总述
  • 求解问题的思路:仅能运用于最有问题
  • 三个条件(goodenotes跳转P256)
    • 可行的:即必须满足问题的约束。
    • 局部最优:它是当前步骤中所有可行选择中最佳的局部选择。
    • 不可取消:即一旦做出选择,在后面的算法步骤中就无法改变了。
Prim算法
  • 算法基本思想:有向连通图 G = < V , E > G=<V,E> G=<V,E>,不断归并顶点。
  • 算法效率:(取决于数据结构)
    • 如果是邻接矩阵,则是 Θ ( ∣ V ∣ 2 ) \Theta(|V|^2) Θ(V2),表示顶点集合数量的平方。
    • 如果是邻接链表,则是 Θ ( ∣ E ∣ log ⁡ ∣ V ∣ ) \Theta(|E|\log|V|) Θ(ElogV)
最小生成树
  • 数据结构老师教的最小生成树:
    • 使用不同的遍历图的方法,可以得到不同的生成树
    • 从不同的顶点出发,也可能得到不同的生成树。
    • 按照生成树的定义,n 个顶点的连通网络的生成树有 n 个顶点、n-1 条边。
  • 五个性质
  • 要会应用
  • 2
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值