概述
基本概念
Θ 渐进紧确界
存在正常量 n 0 , c 1 , c 2,使得
c 1 g ( n ) ≤ f ( n ) ≤ c 2 g ( n ) w h e r e : n ≥ n 0 c_1g(n)\leq f(n)\leq c_2g(n) where :n\ge n_0 c1g(n)≤f(n)≤c2g(n)where:n≥n0
那么称为g ( n ) g(n)g(n)为f ( n ) f(n)f(n)的渐进紧确界,记为:
f ( n ) = Θ ( g ( n ) ) f(n)= \Theta(g(n)) f(n)=Θ(g(n))
同理可理解 O 渐进上界与Ω 渐进下界
递归式时间复杂度
一般情况下,算法的时间递归式都可以写成如下形式:
T
(
n
)
=
a
T
(
n
/
b
)
+
f
(
n
)
T(n)=aT(n/b)+f(n)
T(n)=aT(n/b)+f(n)
表示为:规模为 n,算法所耗费的时间等于划分时间 f(n) 加上同样算法作用在规模为n/b 的时间T(n/b)的 a 倍。
将递归式一直写下去:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1u4EtJoY-1659503636941)(面试准备-排序算法/image-20220802165342258.png)]
可以看出 n == b^m时,问题规模变为1.此时公式变为:
T
(
n
)
=
a
log
b
n
T
(
1
)
+
∑
j
=
0
log
b
n
−
1
a
j
f
(
n
/
b
j
)
T(n) = a^{\log_b n} T(1) + \sum_{j =0}^{\log_b n-1} a^j f(n/b^j)
T(n)=alogbnT(1)+j=0∑logbn−1ajf(n/bj)
因为
log b n log b a = log b a log b n \log_b^{n\log_b^a}=\log_b^a\log_b^n logbnlogba=logbalogbn
log b a log b n = log b n log b a \log_b^{a\log_b^n}=\log_b^n\log_b^a logbalogbn=logbnlogba
所以
a
log
b
n
=
n
log
b
a
a\log_b^n =n\log_b^a
alogbn=nlogba
化简为
T
(
n
)
=
Θ
(
n
log
b
a
)
+
Θ
[
∑
j
=
0
log
b
n
−
1
a
j
f
(
n
/
b
j
)
]
T(n) = \Theta(n^{\log_b^a}) + \Theta[\sum_{j =0}^{\log_b n-1} a^j f(n/b^j)]
T(n)=Θ(nlogba)+Θ[j=0∑logbn−1ajf(n/bj)]
快排举例
递归式
T
(
n
)
=
2
T
(
n
/
2
)
+
Θ
(
n
)
T(n) = 2T(n/2) + \Theta (n)
T(n)=2T(n/2)+Θ(n)
代入之前化简式子为:
T
(
n
)
=
Θ
(
n
)
+
Θ
(
∑
j
=
0
log
2
n
−
1
n
)
=
Θ
(
n
)
+
Θ
(
n
log
2
n
)
=
Θ
(
n
log
2
n
)
=
Ω
(
n
l
o
g
n
)
T(n) = \Theta(n) + \Theta(\sum_{j =0}^{\log_2 n-1}n)=\Theta(n)+\Theta(n\log_2^n)=\Theta(n\log_2^n) = \Omega(nlogn)
T(n)=Θ(n)+Θ(j=0∑log2n−1n)=Θ(n)+Θ(nlog2n)=Θ(nlog2n)=Ω(nlogn)
在n特别大的时候可以忽略底数,因此一般就写作log
堆排序
- 如果为升序排列,将数组
转换为堆之后,从最后一个父节点开始,将最大的数作为父节点,遍历整个堆。 - 将根节点与最后一个结点的位置交换
- 重新形成大顶堆,将排好序的元素排除出堆
- 重复2~3,直到堆只剩下一个元素
//交换位置
void Swap(int* a,int* b){
int temp = *a;
*a = *b;
*b = temp;
}
void MaxHeapify(int arr[], int start,int end){
int dad = start;
int son = dad * 2 - 1;
while(son<=end){
if(son + 1 <=end && arr[son] < arr[son + 1]) son++;
if(arr[dad] > arr[son]) return;
//如果父节点最大就返回,后续重复步骤重新排序时能节省时间
else{
Swap(&arr[dad],&arr[son]);
//继续将其与孙结点比较
dad = son;
son = dad * 2 - 1;
}
}
}
void HeapSort(int arr[],int length){
for(int i = length / 2 - 1; i >= 0; i--){
MaxHeapify(arr,i,length - 1);
}
for(int i = length - 1; i > 0; i--){
Swap(&arr[0],&arr[i]);
MaxHeapify(arr,0,i - 1);
//最后一个元素有序,将堆的长度减少1
}
}
归并排序
归并排序算法将数组分为两半,对每部分递归地应用归并排序。在两部分都排好序后,对它们进行归并。
int min(int x, int y) {
return x < y ? x : y;
}
void merge_sort(int arr[], int len) {
int *a = arr;
int *b = (int *) malloc(len * sizeof(int));
int seg, start;
//从最开始一段只有一个,慢慢merge
for (seg = 1; seg < len; seg += seg) {
for (start = 0; start < len; start += seg * 2) {
//增加每一段的数量,到达下一段
int low = start, mid = min(start + seg, len), high = min(start + seg * 2, len);
//获取接下来的两段
int k = low;
int start1 = low, end1 = mid;
int start2 = mid, end2 = high;
//开始归并
while (start1 < end1 && start2 < end2)
b[k++] = a[start1] < a[start2] ? a[start1++] : a[start2++];
while (start1 < end1)
b[k++] = a[start1++];
while (start2 < end2)
b[k++] = a[start2++];
}
//确保a数组有序,这样才能正确归并
int *temp = a;
a = b;
b = temp;
}
//如果a指向的不是原本数组,那么就是b指向的原有数组。
//但有序的是a,因此要复制到b数组
if (a != arr) {
int i;
for (i = 0; i < len; i++)
b[i] = a[i];
b = a;
}
free(b);
}
递归版:
void merge_sort_recursive(int arr[], int reg[], int start, int end) {
if (start >= end) return;
//只有一个元素时返回
int len = end - start, mid = (len >> 1) + start;
int start1 = start, end1 = mid;
int start2 = mid + 1, end2 = end;
merge_sort_recursive(arr, reg, start1, end1);
merge_sort_recursive(arr, reg, start2, end2);
int k = start;
while (start1 <= end1 && start2 <= end2)
reg[k++] = arr[start1] < arr[start2] ? arr[start1++] : arr[start2++];
while (start1 <= end1)
reg[k++] = arr[start1++];
while (start2 <= end2)
reg[k++] = arr[start2++];
for (k = start; k <= end; k++)
arr[k] = reg[k];
}
void merge_sort(int arr[], const int len) {
int reg[len];
merge_sort_recursive(arr, reg, 0, len - 1);
}
快速排序
- 从数列中挑出一个元素,称为 “基准”(pivot);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;
void swap(int *x, int *y) {
int t = *x;
*x = *y;
*y = t;
}
void quick_sort_recursive(int arr[], int start, int end) {
if (start >= end)
return;
int mid = arr[start];
//一般为了防止最坏情况出现,一般随机选取
int left = start, right = end;
while (left < right) {
while (arr[right] >= mid && left < right)
right--;
while (arr[left] <= mid && left < right)
left++;
if(left < right)
swap(&arr[left], &arr[right]);
}
//和基准值交换,让基准值始终在中间,使其不用再次排序
swap(&arr[left], &arr[start]);
quick_sort_recursive(arr, start, left - 1);
quick_sort_recursive(arr, left + 1, end);
}
void quick_sort(int arr[], int len) {
quick_sort_recursive(arr, 0, len - 1);
}
希尔排序
希尔排序(Shell’s Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本
希尔排序的基本思想是:希尔排序把序列看作是一个矩阵,分成 𝑚 列,逐列进行排序,从某个整数逐渐减为1 ;当 𝑚 为1时,整个序列将完全有序
实例步骤:
void shell_sort(int arr[], int len) {
int gap, i, j;
int temp;
for (gap = len >> 1; gap > 0; gap >>= 1)
for (i = gap; i < len; i++) {
temp = arr[i];
for (j = i - gap; j >= 0 && arr[j] > temp; j -= gap)
arr[j + gap] = arr[j];
arr[j + gap] = temp;
}
}
计数排序
计数排序要求输入的数据必须是有确定范围的整数。当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是 Θ(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。
核心思想:8 个数比 A 小,那 A 就排在第 9 位
//用来计数的数组长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1)
//本例为100
void counting_sort(int *ini_arr, int *sorted_arr, int n) {
int *count_arr = (int *) malloc(sizeof(int) * 100);
int i, j, k;
//初始化计数数组
for (k = 0; k < 100; k++)
count_arr[k] = 0;
for (i = 0; i < n; i++)
count_arr[ini_arr[i]]++;
//count_arr数组由于保存每个元素的第几号小,第一号小的数组下标就为0
for (k = 1; k < 100; k++)
count_arr[k] += count_arr[k - 1];
//有重复时需要特殊处理,这就是为什么最后要反向填充目标数组
//--的目的是为了下一个相同大小元素排序时,排在前面(保证稳定性)
for (j = n; j > 0; j--)
sorted_arr[--count_arr[ini_arr[j - 1]]] = ini_arr[j - 1];
free(count_arr);
}
先找到最大值max和最小值min,然后呢,从索引0开始依次存放3~8出现的次数,每个次数累加上其前面的所有次数,得到的就是元素在有序序列中的位置信息。
- 如果元素不重复,array中的元素 k 对应的 counts 索引是
k – min
- 如果重复,array中的元素 k 对应的 counts 索引是 k – min-p,p 代表着是倒数第几个 k
桶排序
桶排序是计数排序的升级版。基本思想是将一个数据表分割成许多buckets,然后每个bucket各自排序
为了使桶排序更加高效,我们需要做到这两点:
- 在额外空间充足的情况下,尽量增大桶的数量
- 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
public static void bucketsort(int[] arr) {
ArrayList bucket[] = new ArrayList[5];// 声明五个桶
for (int i = 0; i < bucket.length; i++) {
bucket[i] = new ArrayList<Integer>();// 确定桶的格式为ArrayList
}
for (int i = 0; i < arr.length; i++) {
// 确定元素存放的桶号
int index = arr[i] / 10;
// 将元素存入对应的桶中
bucket[index].add(arr[i]);
}
for (int i = 0; i < bucket.length; i++) {
// 遍历每一个桶
// 对每一个桶排序,但元素分配不均匀时,算法退化到桶内使用的算法
bucket[i].sort(null);
}
}
基数排序
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,依次对个位数、十位数、百位数、千位数、万位数…进行排序(从低位到高位)
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:
- 基数排序:根据键值的每位数字来分配桶;
- 计数排序:每个桶只存储单一键值;
- 桶排序:每个桶存储一定范围的数值;
void radixsort(int *a, int n) {
//MAX代表最大为多少
int i, b[MAX], m = a[0], exp = 1;
for (i = 1; i < n; i++) {
if (a[i] > m) {
m = a[i];
}
}
while (m / exp > 0) {
int bucket[10] = { 0 };
for (i = 0; i < n; i++) {
bucket[(a[i] / exp) % 10]++;
}
//和桶排序思想类似,记录序号
for (i = 1; i < 10; i++) {
bucket[i] += bucket[i - 1];
}
for (i = n - 1; i >= 0; i--) {
b[--bucket[(a[i] / exp) % 10]] = a[i];
}
for (i = 0; i < n; i++) {
a[i] = b[i];
}
exp *= 10;
}
}