外部排序
对内部排序的分析
方法 时间负责度 空间复杂度 稳定性
直接插入 n^2 1 稳定
折半插入 n^2 1 不稳定/稳定(一般)
希尔排序 nlog2n 不稳定
二路插入 n^2
直接选择 n^2 不稳定
二路选择 n^2 不稳定
冒泡 n^2
鸡尾酒 n^2
堆 nlog2n 不稳定
二叉树 nlog2n 不稳定
快速 nlog2n nlog2n o(n)书上空间 不稳定
归并 nlog2n nlog2n 稳定
基数 d(n+rd) r+n 稳定
计数 n max-min+1 没有稳不稳定这个说法 因为根据下标来
外部排序
内存有限,数据量过大,数据不能一次性加载到内存中
数据保存在外存中(硬盘,文件里面)上
限制外部排序效率的因素是IO(读写的速率)的效率
所以提高外部排序的效率的方法是减少IO次数
寄存器>cache>内存>固态>机械
思路:
二路归并(又或者说是m路归并)
第一次先按块加载进来,使用内部排序中的任意一种进行排序,再写回到原文件
外部排序读写的次数与归并的趟数成正比
每归并一趟,把所有的数据加载进来,并且写回来
总共进行多少趟归并 log2(n/k) logm r r=n/k m就是归并路数,2即路数为2
n是总的数据个数,k是每个内存块中存储数据的个数
n/k == 数据块 初始归并段的数量r,初始归并不算入次数
提高外部排序的效率:
1.多路归并,增加归并路数 有败者树这个概念
败者树的作用:减少多路归并时比较的次数
除了第一次比较以外,最多需要logm次比较
因为第一次选出后,其他队的比较结果其实还是和上一次一样的,
除了赢的那一队的第一位胜者出,该队下一个队员和之前比较的那组再进行比较,
所以其实少了最基础的其他队比较的环节,因此减少了比较次数。
2.减少初始归并段 (即内存扩大,数据存储的个数增多)
用 置换选择排序 来初始化归并段
所以我们有 最佳归并树 这个概念 (根据初始归并段构造最佳归并树,减少读写磁盘次数)
用置换-选择排序, 初始化的每个归并段中数据的个数不一样
用这些归并段来构造一个m叉树,要使得读写次数最少
需要构造一棵最优m叉树,即最佳归并树
例如归并段:
9 30 12 18 3 17 2 6 24 这个是每组里的个数,总共9组
三路平衡三叉树的次数:
(9+ 30 +12+ 18+ 3+ 17+ 2 +6 +24)*2*2=484
第一个2是路径长度,路径长度不算自己所在那层
虚段:数量为0
为了构造m叉这样的最佳归并树,可能需要增加虚段(0);
因为归并段和归并路数相关
9 12 18 3 17 2 6 24 8,3路归并时,8个段没法构成三叉最优归并树
9 12 18 3 17 2 6 24 0 9,所以补了一个虚段
5
0 2 3
0就是虚段
其实是哈夫曼树的知识点(WPL带权路径最短 )
归并段是否补充及补充多个虚段的推解
假设对m个归并段,进行k路归并,使读写次数最少,得构造最优归并树
需要补充多少个虚段?
9个归并段 3路归并
8个归并段 3路补充一个虚段
7个归并段 3路不用补
10个 4路不用补
11个 4路补
所以
(m-1)%(k-1)==0不需要补充虚段 m是叶子结点的个数。除了叶子结点以外,所有结点需要k个子结点
n个度为k的结点,m个度为0的结点
结点个数=总度数+1
nk+1:这是所有的结点数 nk+1=n+m;
(k-1)n=m-1; //n肯定是整数 n=(m-1)/(k-1)
如果要补 要补k-(m-1) % (k-1)-1个虚段
即 k-1 – (m-1)%(k-1)
推证:
如果x/y!=0;
即x%y有余数
(x-x%y+y)%y==0
(x+(y-x%y))%y==0;
所以补了y-x%y这些虚段
所以是k-1-(m-1)%(k-1)
搜索
查找表
静态查找表:若对查找表只作查找和检索的操作
1.顺序查找
这个顺序查找的写法比较新颖
动态查找表:除了查找和检索的操作,进行插入和删除
2.有序表查找
递推二分查找
int bin_search(int arr[],int n,int key){
int left = 0,right = n-1;
while(left<=right){
int mid = (left+right)/2;
if(arr[mid] == key){
while(arr[mid-1]==arr[mid])
mid--;
return mid;
}
if(key < arr[mid]){
right = mid - 1;
}else{
left = mid + 1;
}
}
return -1;
}
递归二分查找
int binSearch(int arr[],int left,int right,int key){
if(left>right)
return -1;
int mid = (left+right)/2;
if(arr[mid]==key)
return mid;
if(key<arr[mid])
return binSearch(arr,left,mid-1,key);
return binSearch(arr,mid+1,right,key);
}
int bin_search(int arr[],int n,int key){
return binSeach(arr,0,n-1,key);
}
ASL:
查找算法衡量好坏的依据为:查找成功时,查找的关键字和查找表中的数据元素中进行过比较的
个数的平均值,称为平均查找长度(Average Search Length,用 ASL 表示)
1 5 9 2 4
ASL = (1+2+3+4+5 +5*5)/10
ASL成功 = 1/5 + 2/5 + 3/5 + 4/5 + 5/5
ASL失败 = 5
1查找了4次,5查找了2次,9查找了3次,2次了1次,4查找了0次
10次 4/10
ASL成功 = 1*4/10 + 2*2/10 + 3*3/10 + 4*1/10 + 5*0/10
= (4 + 2*2 + 3*3 + 4*1 + 5*0)/10
ASL=概率*比较次数
单向链表不能用bsearch,因为链表不连续 这些都要连续的
man bsearch //库中包含了二分查找这个函数
void *bsearch(const void *key,const void *base,size_t nmemb,size_t size,
int (*comp)(const void *,const void *));
折半查找判定树
PH值取最小的二叉树为静态最优查找树
带权内路径长度之和PH(和平均查找长度成正比)值
ASL=(1+2+3+4+5+5*5)/10
B- B+ 树
B树的深度要比表面上给的+1,因为图上表示的最后一层下面还有看不见的NULL层
m阶B-插入:
插入到叶子结点,如果叶子结点关键字等于m个,需要分裂
把[m/2]关键字上提,如果上提导致父结点关键字等于m个
需要继续向上分裂
m阶B-删除:
删除非叶子结点关键字,需要用左边树中最大值来替换
删除左边树中最大值(叶子结点)
删除之后,结点关键字个依然还够[m/2]个,不需要处理
如果个数不够[m/2],则考虑向左或者向右借
如果左右不够借的情况,向左/右合并
B+树
一颗 m 阶的 B+树和 m 阶的 B-树的差异在于:
有 n 棵子树的结点中含有 n 个关键字;
在上一节中,在 B-树中的每个结点关键字个数 n 的取值范围为⌈m/2⌉ -1≤n≤m-1,而在
B+树中每个结点中关键字个数 n 的取值范围为:⌈m/2⌉≤n≤m。
所有的叶子结点中包含了全部关键字的信息,及指向含这些关键字记录的指针,且叶子结点本身
依关键字的大小自小而大顺序链接。
所有的非终端结点(非叶子结点)可以看成是索引部分,结点中仅含有其子树(根结点)中的最
大(或最小)关键字。
哈希表:(又称散列表,是一种数据结构)
特点是:数据元素的关键字与其存储地址直接相关,通过散列函数(哈希函数)f(key)
映射到其存储地址哈希查找理想的情况是希望不经过任何比较,一次存取便能得到所查记录,但实际的情况是,
对不同的关键字可能得到同一哈希地址,即key1!=key2,而f(key1) =f(key2),这种现象称为冲突(collision)
具有相同函数值的关键字(key1,key2)对该哈希函数来说称做同义词(synonym)。
冲突越多,查找效率越低
处理冲突的方法有:
1.开放定址法
2.再哈希法
3.链地址法
4.建立一个公共溢出区
哈希函数H(key)=key MOD 13
哈希查找效率最高 O(1) 没有冲突的情况下
线性探测再散列方法构造哈希表
%求余,被占往后放
失败次数
(0+12+11..+1)/13
成功次数
索引顺序表查找
朴素查找(BF算法):在str中查找childstr,如果找到返回下标位置,没有返回-1
#include<stdio.h>
#include<string.h>
int find(const char *str,const char *childstr){
int i;
int lenchild=strlen(childstr);
int lenstr=strlen(str);
for(i=0;i<lenstr;i++){
int j=0;
int k=i;
if(str[k]==childstr[j]){
while(str[k++]==childstr[j++]);
}
j--;
if(j==lenchild){
return i;
}
}
return -1;
}//这是自己写的方法
int find(const char *str,const char *childstr){
int i,j=0;
for(i=0;str[i]!='\0'&&childstr[j]!='\0';){
if(str[i]==childstr[j]){//如果相等继续比较下一个
i++;
j++;
}else{//如果不相等,str回溯到 j 个字符串之前+1
i=i-j + 1;//不相等的时候 主串回溯
j=0;
}
}
if(childstr[j]=='\0') return i-j;
return -1;
}//这是新方法
//for(i=0;str[i]!=’\0’;i++)
//if(strncmp(str+i,childstr,len)==0 return i //这是如果可以使用strncmp的关键步骤
//这个时间复杂度O(nm) n str的长度 m childstr的长度
int main(){
int ret=find("asdafa","af");
printf("%d\n",ret);
return 0;
}
KMP算法
快速模式匹配算法,简称 KMP(D.E.Knuth,J.H.Morris和V.R.Pratt) 算法
朴素查找会回溯,时间复杂度为O(nm) n为主串长度,m为子串长度
而kmp算法时间复杂度为O(n+m), 主串不回溯 ,仅调整j的位置,即子串的位置,kmp会算定j回退的距离
//kmp O(n+m) 主串不回溯 调整j的位置
//当childstr[j] 和 主串不相等时 j退回到哪一个字符进行比较
void getNext(const char *pattern,int next[]){
next[0] = -1;
int len = strlen(pattern);
int j=0,k=-1;//next[j] pattern[0,j-1] 最大重复子串
while(j<len-1){
if(k==-1 || pattern[j] == pattern[k]){
//next[++j] = ++k;
//优化next数组
if(pattern[++j] == pattern[++k]){
next[j] = next[k];
}else{
next[j] = k;
}
}else{//前缀的最后一个字符和后缀最后一个字符不相等
k = next[k];
}
}
}
//str主串 pattern模式串
int kmp(const char *str,const char *pattern){
int i=0,j=0;
int next[strlen(pattern)];
getNext(pattern,next);
while(str[i]!='\0'&&pattern[j]!='\0'){
//j==-1 第一个字符就不相等 只能改变j不能改变i
if(j==-1||str[i]==pattern[j]){
++i;
++j;
}else{
j = next[j];//j回退到指定的位置继续比较
}
}
}
这个版本的KMP算法对getNext函数的核心算法会浪费时间的问题已经进行了优化
kmp getNext()核心算法推解
模式串移动距离的判断每次模式匹配失败后,计算模式串向后移动的距离是 KMP 算法中的核心部分。
ABCAB D 其实是D这个位置不同
前缀 但是最后那个不取 [0,j-1
A
AB
ABC
ABCA
后缀 最前那个不取
BCAB
CAB
AB
B
前缀和后缀相等的最大长度
AB所以是2
全排列
全排列 1 2 3 4 4*3*2*1
假设数组 总共n个数据,现在还要m个数全排列
void showArr(int arr[],int n){
int i;
for(i=0;i<n;i++){
printf("%d ",arr[i]);
}
printf("\n");
}
void swap(int *pa,int *pb){
int tmp = *pa;
*pa = *pb;
*pb = tmp;
}
//数组总共n个数据 m个数需要进行全排列
void showAll(int arr[],int n,int m){
if(m==1){
showArr(arr,n);
}else{
int i;
for(i=m-1;i>=0;i--){
swap(&arr[i],&arr[m-1]);//固定最后一个值
showAll(arr,n,m-1);//把剩下的m-1个数再进行全排列
swap(&arr[i],&arr[m-1]);//恢复原样,不影响下一种情况
}
}
}
//对n个数进行全排列
void pailie(int arr[],int n){
// (1 2 3全排列) 4
// (1 2 4全排列) 3
// (1 3 4全排列) 2
// (2 3 4全排列) 1
showAll(arr,n,n);
}
//全排列 1 2 3 4 4*3*2*1 10