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,c和n0,使得对所有的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):存在c和n0,使得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):存在c和n0,使得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)
单独讲