数据结构与算法学习笔记11:二叉树层打印/跳表/冒泡排序/选择排序/插入排序/希尔排序/计数排序
二叉树按层打印
如何将一颗二叉树按层打印,一层输出到一行上?
- 方案一:加特殊符号
- 方案二:双队列,dad一个队,son一个队
- 方案三:计数
- 方案四:记录每个结点的层数or深度
- 方案五:把树的NULL结点都补上补成满二叉树然后一层层出(注意不要破坏树的结构,不要在原有的树里面加,而是在外面用别的容器如队列,假装补充而已)
跳跃列表(Skip List)
随机结构的有序链表
通过建立索引的方式,对于数据量越大的有序链表,通过建立多级索引,查找效率提升会非常明显。
一般高度都设定为 l o g 2 n log_2 n log2n!
- 特点:
- 每个元素插入时随机生成它的level;
- 最底层包含所有的元素;
- 每个索引节点包含两个指针,一个向下,一个向右;
- 跳表查询、插入、删除的时间复杂度为 O ( l o g 2 n ) O(log_2 n) O(log2n)
二分法也是 l o g 2 n log_2 n log2n,那么为什么要用跳表呢?
- 二分,其添加或者删除的时候会影响二分的对应关系,后面的全部要调整…插入的时间很有可能变成 O ( n ) O(n) O(n)
冒泡排序(Bubble Sort)
核心思想:比较相邻的元素。如果第一个比第二个大,就交换他们两个。
若文件的初始状态是正序的,一趟扫描即可完成排序。所需的关键字比较次数C和记录移动次数M均达到最小值: C m i n = n − 1 , M m i n = 0 C_{min}=n-1,M_{min}=0 Cmin=n−1,Mmin=0,所以,冒泡排序最好的时间复杂度为 O ( n ) O(n) O(n)。
若初始文件是反序的,需要进行
n
−
1
n-1
n−1趟排序。每趟排序要进行
n
−
i
n-i
n−i次关键字的比较(
1
≤
i
≤
n
−
1
1≤i≤n-1
1≤i≤n−1),且每次比较都必须移动记录三次来达到交换记录位置。在这种情况下,比较和移动次数均达到最大值:
C
m
a
x
=
n
(
n
−
1
)
2
=
O
(
n
2
)
M
m
a
x
=
3
n
(
n
−
1
)
2
=
O
(
n
2
)
C_{max}=\frac{n(n-1)}{2}=O(n^2)\\ M_{max}=\frac{3n(n-1)}{2}=O(n^2)
Cmax=2n(n−1)=O(n2)Mmax=23n(n−1)=O(n2)
冒泡排序的最坏时间复杂度为
O
(
N
2
)
O(N^2)
O(N2),综上,因此冒泡排序总的平均时间复杂度为
O
(
N
2
)
O(N^2)
O(N2)。
冒泡排序在排序的过程中,不需要占用很多额外的空间(就是在交换元素的时候需要临时变量存一存,这里需要的额外空间开销是常量级的),因此冒泡排序的空间复杂度为 O ( 1 ) O(1) O(1)了。
#include <stdio.h>
void BubbleSort(int arr[],int nLength){
if(arr == NULL || nLength <= 0)
return;
int i,j,temp;
for (i = 0; i < nLength - 1; i++) {
for(j = 0; j < nLength -1 - i;j++){
if (arr[j]>arr[j + 1]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
void Print(int arr[],int nLength){
if(arr == NULL || nLength <= 0)
return;
for (int i = 0; i < nLength; i++) {
printf("%d ",arr[i]);
}
printf("\n");
}
int main(){
int arr[] = {10,7,8,19,2,11,66,4};
BubbleSort(arr, sizeof(arr)/sizeof(arr[0]));
Print(arr, sizeof(arr)/sizeof(arr[0]));
return 0;
}
优化
-
如果后面有一部分已经有序其实不需要这么多次排序,可以用一个标记记录当前组最后一次交换的下标,以其作为下一次循环的结束边界,可以避免一些无意义的比较。如果没有发生交换,不标记,也就是说此时数组已经有序,直接跳出循环。
-
分析:
i的范围为0到n-2,共需要完成n-1次遍历,其关系为n-2-0+1=n-1
而第i层排序的范围为i到n-2,共需要完成index-1次遍历,则n关系为n-2-i-1=index-1,得到i=n-index
而j<n-1-i,带入上式的i=n-index,可以得到j<index-1
- 代码:
#include <stdio.h> void BubbleSort(int arr[],int nLength){ if(arr == NULL || nLength <= 0) return; int i,j,temp; int index; int count = 0; for (i = 0; i < nLength - 1; i++) { index = 0; for(j = 0; j < nLength -1 - i;j++){ if (arr[j]>arr[j + 1]) { temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; index = j + 1; } count++; } if (index == 0) break; i = nLength - index - 1; //因为上面的for里要i++,所以先减掉一个 } printf("%d\n",count); } void Print(int arr[],int nLength){ if(arr == NULL || nLength <= 0) return; for (int i = 0; i < nLength; i++) { printf("%d ",arr[i]); } printf("\n"); } int main(){ int arr[] = {10,7,8,19,2,11,66,74,88}; BubbleSort(arr, sizeof(arr)/sizeof(arr[0])); Print(arr, sizeof(arr)/sizeof(arr[0])); return 0; } //30次->15次
选择排序(Selection sort)
第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。选择排序是不稳定的排序方法。
-
平均时间复杂度: O ( N 2 ) O(N^2) O(N2)
最佳时间复杂度: O ( N 2 ) O(N^2) O(N2)
最差时间复杂度: O ( N 2 ) O(N^2) O(N2)
空间复杂度: O ( 1 ) O(1) O(1)选择排序的交换操作介于$ 0 $和 ( n − 1 ) (n - 1) (n−1)次之间。选择排序的比较操作为 n ( n − 1 ) 2 \frac{n (n - 1)}{2} 2n(n−1)次之间。选择排序的赋值操作介于 0 0 0 和 3 ( n − 1 ) 3 (n - 1) 3(n−1) 次之间。总的比较次数 N = ( n − 1 ) + ( n − 2 ) + . . . + 1 = n ( n − 1 ) 2 N=(n-1)+(n-2)+...+1=\frac{n (n - 1)}{2} N=(n−1)+(n−2)+...+1=2n(n−1)。交换次数 O ( n ) O(n) O(n),最好情况是,已经有序,交换0次;最坏情况交换 n − 1 n-1 n−1次,逆序交换 n 2 \frac{n}{2} 2n次。交换次数比冒泡排序少多了,由于交换所需CPU时间比比较所需的CPU时间多,n值较小时,选择排序比冒泡排序快。
#include <stdio.h>
void SelectionSort(int arr[],int nLength){
if(arr == NULL || nLength <= 0)
return;
int i,j;
int min;
for (i = 0; i < nLength -1; i++) {
min = i;
for (j = i + 1; j < nLength; j++) {
if (arr[min] > arr[j]) {
min = j;
}
}
//将最小值放入
int temp;
temp = arr[min];
arr[min] = arr[i];
arr[i] = temp;
}
}
void Print(int arr[],int nLength){
if(arr == NULL || nLength <= 0)
return;
for (int i = 0; i < nLength; i++) {
printf("%d ",arr[i]);
}
printf("\n");
}
int main(){
int arr[] = {10,7,8,19,2,11,66,74,88};
SelectionSort(arr, sizeof(arr)/sizeof(arr[0]));
Print(arr, sizeof(arr)/sizeof(arr[0]));
return 0;
}
插入排序(Insertion Sort)
基本思想是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增 1 的有序表。(也就是:将待排序数据分成两个部分,一部分有序,一部分无序,将无序元素依次插入到有序中去,完成排序)
-
插入排序的平均时间复杂度是 O ( N 2 ) O(N^2) O(N2),空间复杂度为常数阶 O ( 1 ) O(1) O(1),具体时间复杂度和有序性有关联。
插入排序中,当待排序数组是有序时,是最优的情况,只需当前数跟前一个数比较一下就可以了,这时一共需要比较 N − 1 N-1 N−1次,时间复杂度为 O ( N ) O(N) O(N)。最坏的情况是待排序数组是逆序的,此时需要比较次数最多,最坏的情况是 O ( N 2 ) O(N^2) O(N2)。
步骤:
1、分为有序/无序俩部分
2、无序插入到有序中去(倒序遍历有序的)
- (1)保存无序元素
- (2)倒序遍历,依次比较:小的话则前一个元素向后移动,大的话就把当前元素插入即可
#include <stdio.h>
void InsertionSort(int arr[],int nLength){
if(arr == NULL || nLength <= 0)
return;
int i,j;
i = 1;
int temp;
//无序元素插入
for (i = 0; i < nLength; i++) {
j = i - 1;
temp = arr[i];
//倒序遍历有序数组 插入无序元素
//while(temp < arr[j] && j >= 0){
while(j >= 0 && temp < arr[j]){
//不可能无休止的往前遍历,注意有序数组j限制
//移动
arr[j + 1] = arr[j];
j--;
//注意 j--后,j = -1时
//temp < arr[j] && j >= 0中temp < arr[j]会产生越界访问
//所以需要改成j >= 0 && temp < arr[j]
}
//插入
arr[j + 1] = temp;
}
}
void Print(int arr[],int nLength){
if(arr == NULL || nLength <= 0)
return;
for (int i = 0; i < nLength; i++) {
printf("%d ",arr[i]);
}
printf("\n");
}
int main(){
int arr[] = {10,7,8,19,2,11,6,4,88};
InsertionSort(arr, sizeof(arr)/sizeof(arr[0]));
Print(arr, sizeof(arr)/sizeof(arr[0]));
return 0;
}
- 插入排序适用于:数据少/调整范围小的场景
希尔排序/缩小增量排序(Shell Sort)
内涵了插入排序的思想,以分块的形式放开了数据量的限制,是插入排序的优化体。把记录按下标的一定增量分组,对每组使用直接插入排序算法排序。
-
shell排序的时间复杂度是根据选中的 增量d 有关的,所以分析shell排序的时间复杂度是个比较麻烦的事;只给出答案,不会推算;
在最优的情况下,时间复杂度为: O ( N 1.3 ) O(N^{1.3}) O(N1.3) (元素已经排序好顺序)
在最差的情况下,时间复杂度为: O ( N 2 ) O(N^2) O(N2)
-
空间复杂度为常数阶 O ( 1 ) O(1) O(1)。
-
步骤过程:
1、确定Gap
2、分组
0 0+Gap 0+Gap+Gap…… < n
1 1+Gap 1+Gap+Gap…… < n
……
3、各组进行插入排序(此时的调整位数应为+Gap or -Gap)
4、缩减Gap继续重复23步骤,最后一次分组Gap=1的插入排序进行完后算法结束。
-
代码:
#include <stdio.h> void ShellSort(int arr[],int nLength){ if(arr == NULL || nLength <= 0) return; int i,j; int nGap; int k; int temp; //确认间隔,此程序以2分方式为例 for (nGap = nLength / 2; nGap >= 1; nGap /= 2) { //组 for (i = 0; i < nGap; i++) { //各组内插入排序 for (j = i + nGap; j < nLength; j += nGap) { k = j - nGap; //有序的最后一个元素 temp = arr[j]; while (k >= i && temp < arr[k]) { arr[k + nGap] = arr[k]; //移动 k -= nGap; //往前走 } //插入 arr[k + nGap] = temp; } } } } void Print(int arr[],int nLength){ if(arr == NULL || nLength <= 0) return; for (int i = 0; i < nLength; i++) { printf("%d ",arr[i]); } printf("\n"); } int main(){ int arr[] = {10,7,8,19,2,11,66,74,88}; ShellSort(arr, sizeof(arr)/sizeof(arr[0])); Print(arr, sizeof(arr)/sizeof(arr[0])); return 0; }
-
实际上,并不需要一定等第一组执行完后再对第二组排序,多组可以同时进行排序,nGap间隔进行并不相互影响,所以在代码上还可进行一些优化,可以少写一层循环,提高系统的效率,不过时间效率上是一样的。
for (i = nGap; i < nLength; i++) { j = i - nGap; temp = arr[i]; while(j >= 0 && temp < arr[j]){ arr[j + nGap] = arr[j]; j -= nGap; } //插入 arr[j + nGap] = temp; }
计数排序(Counting Sort )
适用于有一定区间,多重复,数据差值小的情况。是基于非比较的排序。
-
过程:
1、Max-Min确定区间范围,申请计数器数组
2、计数
3、输出
-
优化:如果是成绩排名,或者有对应情况的,那么只输出值会失去原有信息,所以要优化
1、Max-Min确定区间范围,申请计数器数组
2、计数,累加
3、申请新空间,然后倒序遍历数据,根据计数器数组累加后得到的排名放入对应位置
#include <bits/types/FILE.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void CountingSort(int arr[],int nLength){
if(arr == NULL || nLength <= 0)
return;
//最大值最小值的查找,确定范围
int nMax = arr[0];
int nMin = arr[0];
for (int i = 1; i < nLength; i++) {
if (arr[i] > nMax) {
nMax = arr[i];
}
if (arr[i] < nMin) {
nMin = arr[i];
}
}
//计数数组
int *pCount = NULL;
pCount = (int*)malloc(sizeof(int)*(nMax - nMin + 1));
memset(pCount,0,sizeof(int)*(nMax - nMin + 1));
//计数
for (int i = 0; i < nLength; i++) {
pCount[arr[i] - nMin]++;
}
//累加
for (int i = 1; i < nMax - nMin + 1; i++) {
pCount[i] += pCount[i - 1];
}
//申请新空间 存储
int *pNew = (int*)malloc(sizeof(int)*nLength);
int index;
for (int i = nLength - 1; i >= 0; i--) {
//pNew[pCount[arr[i] - nMin] - 1] = arr[i];
//pCount[arr[i] - nMin] 累加后得到的排名
//-1是因为pNew数组下标
//因为存储完对应计数器要--,所以加一个index
index = pCount[arr[i] - nMin] - 1;
pCount[arr[i] - nMin]--;
pNew[index] = arr[i];
}
//pNew就是要得到的排序结果,可以根据情况看要不要拷贝覆盖源数据
//如果要拷贝
for (int i = 0; i < nLength; i++) {
arr[i] = pNew[i];
}
free(pNew);
pNew = NULL;
}
void Print(int arr[],int nLength){
if(arr == NULL || nLength <= 0)
return;
for (int i = 0; i < nLength; i++) {
printf("%d ",arr[i]);
}
printf("\n");
}
int main(){
int arr[] = {90,87,98,89,92,91,96,94,98};
CountingSort(arr, sizeof(arr)/sizeof(arr[0]));
Print(arr, sizeof(arr)/sizeof(arr[0]));
return 0;
}