排序算法

内部排序

  • 这里先介绍一个概念,算法稳定性
  • 算法稳定性 -- 假设在数列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;并且排序之后,a[i]仍然在a[j]前面。则这个排序算法是稳定的!
// O(N2), 稳定的算法
void bubble_sort(int * arr,size_t n){
    bool sort =false;
    for(int i=0;i<n;++i){
        for(int j=1;j<n-i;++j){
            if(arr[j]<arr[j-1]){
                swap...;
                sort=true;
            }
            if(!sort) break;
        }
    }
}

// O(N2) 稳定的算法
// 分左右两个,左右有序,右边无序,无序,插入到有序中
void insert_sort(int * arr,size_t n){
    for(int i=1;i<n;++i){
    	int key = arr[i];
        for(int j=i;j>0 && key<arr[j];--j) // 从右往左找插入位置
			arr[j+1] = arr[j];            
        if(j+1 != i) arr[j+1] = key; // 节省一次赋值的消耗
    }
}
// 二分插入排序,优化了查找插入位置的时间
void bin_insert_sort(int * arr,size_t n){
    for(int i=1;i<n;++i){
        int L,R=i-1,key = arr[i];
        while(L<R){
           	int mid = L + R >> 1;
            if(arr[mid] <= key) L = mid+1;
            else R = mid-1;
        } // arr[L] <= key; 
        for(int j=j-1;j>L;--j) arr[j+i] = arr[j];
        arr[L] = key;
    }
}

void two_road_insert_sort(int * arr,size_t n){
    
}

/* 
希尔排序的时间复杂度与增量(即,步长gap)的选取有关。
例如,当增量为1时,希尔排序退化成了直接插入排序,此时的时间复杂度为O(N²)
而Hibbard增量的希尔排序的时间复杂度为O(N^3/2^)。
不稳定的算法
*/
void shell_insert_sort(int * arr,size_t n){
    int i,j,gap; // gap 步长
    for(gap=n/2;gap>0;gap/=2){
        for(i=gap;i<n;++i){ // 直接插入排序
	        int key =arr[i];
            // 每次减去步长gap,
            for(j=i-gap;j>=0&&key<arr[j];j-=gap) arr[j+gap] = arr[j];
            if(j+gap!=i) arr[j+gap]=key;
        }
    }
}

// O(N2) 稳定
// 选择排序  从key后面的序列中,找到比key小的,然后交换
void select_sort(int *arr, size_t n){
    int i;        // 有序区的末尾位置
    int j;        // 无序区的起始位置
    int min;    // 无序区中最小元素位置
    for(i=0; i<n; i++){
        min=i;
        // 找出"a[i+1] ... a[n]"之间的最小元素,并赋值给min。
        for(j=i+1; j<n && arr[j]<arr[min]; j++) min=j;
        // 若min!=i,则交换 a[i] 和 a[min]。
        // 交换之后,保证了a[0] ... a[i] 之间的元素是有序的。
        if(min!=i) swap(arr[i], arr[min]);
    }
}
// 快排	最坏情况下是O(N2),平均的时间复杂度是O(N*lgN)	不稳定的算法
/* 假设被排序的数列中有N个数。遍历一次的时间复杂度是O(N),至少lg(N+1)次,最多N次。
快速排序是采用的分治法进行遍历的,我们将它看作一棵二叉树,它需要遍历的次数就是二叉树的深度
而根据完全二叉树的定义,它的深度至少是lg(N+1)。因此,快速排序的遍历次数最少是lg(N+1)次。
*/
void quick_sort(int * arr,size_t n){
	if(n<=1) return;
    int ket= arr[0];
    int L=0,R=n-1;
    while(L<R){
        while(L<R && key<=arr[R]) --R; //从右向左找第一个小于key的数
        if(L<R) arr[L++]=arr[R];   
        while(L<R && arr[L]<=key) ++L; //从右向左找第一个大于key的数
        arr[R]=arr[L];
        if(L<R) arr[R--]=arr[L];
    }
    arr[L] = key;
    // [0,L-1] L-1>0
    if(L>1) quick_sort(arr,L);
    // [L+1,n-1] n>L+1   为什么不是 n-1>L+1 
    if(L+1<n) quick_sort(arr+L+1,n-L-1);
}

/* 
堆排序	O(N*lgN)	
	不稳定算法原因: 
	交换数据是比较父结点和子节点之间的数据,
	所以,即便是存在两个数值相等的兄弟节点,它们的相对顺序在排序也可能发生变化
实现原理:
	
*/
void reheap(int * arr,int pos,size_t n){
    // pos ,要调节的元素下标
    int key = arr[pos];
    int child = 2*pos +1; // pos 左孩子
    while(child < n){// 左孩子存在
        //右孩子存在且右孩子大
        if(child+1<n && arr[child]<arr[child+1]) ++child;
        if(key < arr[child]){ // 于较大孩子交换值 
            arr[pos] = arr[child];
            pos = child;
            child = 2*pos +1;
        }else break;
    }
    arr[pos] = key; 
}
void heap_sort(int * arr,size_t n){
    int i;
    //调整成大根堆 从最后一个非叶子节点开始调整
    for(i=n/2-1;i>=0;--i) reheap(arr,i,n);
    for(i=n-1;i>0;--i){
        //把arr[0]和最后一个元素交换位置 
		swap(arr[0],arr[i]);
		//0下标元素发生改变  只需要重新调整0位置的元素即可 
		reheap(arr,0,i);
    }
}

// 鸡尾酒排序	
void cocktail_sort(int * arr,size_t n){
    int i,j;
	for(i=0;i<n/2;++i){//进行n/2次循环  每次选择最大值  和 最小值  下标
		int minIndex = i;
		int maxIndex = i;
		for(j=i+1;j<n-i;++j){
			if(arr[j]<arr[minIndex]) minIndex = j;
			else if(arr[maxIndex] < arr[j]) maxIndex = j;
		} 
		int tmp = 0;
		if(minIndex != i){
			tmp = arr[minIndex];
			arr[minIndex] = arr[i];
			arr[i] = tmp;
		}
		if(maxIndex == i) maxIndex = minIndex; 
		if(maxIndex != n-i-1){
			tmp = arr[maxIndex];
			arr[maxIndex] = arr[n-i-1];
			arr[n-i-1] = tmp;
		}
	}
}
/*
    归并排序 O(N*lgN),且是稳定算法
    1. 分解 -- 将当前区间一分为二,即求分裂点 mid = (low + high)/2;
    2. 求解 -- 递归地对两个子区间a[low...mid] 和 a[mid+1...high]进行归并排序。递归的终结条件是子区间长度为1。
    3. 合并 -- 将已排序的两个子区间a[low...mid]和 a[mid+1...high]归并为一个有序的区间a[low...high]。
*/
// 这里用的算法,是直接传递数组首地址加进步值,整体思路一致
void merge_part_sort(int * arr,size_t n){
    const int mid = n/2; // 记录分界线
	int brr[mid];//用于存储[0,mid) 数据
	int i,j,k;
	for(i=0;i<mid;++i) brr[i] = arr[i];
	//i记录元素比较后放置的位置 j记录brr的下标位置  k记录的是arr[mid-n)部分下标 
	i=0,j=0,k=mid;
    while(j<mid && k<n){ //brr[0,mid)和arr[mid-n)两部分数据都还有
        if(brr[j] < arr[k]) arr[i++] = brr[j++]; //[0,mid) < [mid,n)
        else arr[i++] = arr[k++]; //[0,mid) >= [mid,n)
    }
    while(j<mid) arr[i++] = brr[j++];
    // arr[k,n) 本身就是升序的,所以不需要管
}
void merge_sort(int * arr,size_t n){
	if(arr==NULL || n<=1) return;
	int mid = n/2;  //[0,mid)   [mid,n)
	merge_sort(arr,mid);  //[0,mid) 区间中的元素排序 
	merge_sort(arr+mid,n-mid);  //[mid,n) 区间中的元素排序 
	merge_part_sort(arr,n); // 区间中的元素排序
}

/**
 * 	桶排序
 *     	n -- 数组a的长度
 *     	max -- 数组a中最大值的范围
 */
void bucketSort(int * arr, size_t n, int max)
{
    if (arr==NULL || n<1 || max<1) return ;
    int i,j;
    int buckets[max];
    memset(buckets, 0, max*sizeof(int));
    // 1. 计数
    for(i = 0; i < n; i++) buckets[a[i]]++; 
    // 2. 排序
    for (i = 0, j = 0; i < max; i++) 
    {
        while( (buckets[i]--) >0 ) // 桶内元素个数>0
            arr[j++] = i;
    }
}

/* 
	基数排序
	MSD:先从高位开始进行排序,在每个关键字上,可采用桶排序
	LSD:先从低位开始进行排序,在每个关键字上,可采用计数排序
*/
void radix_sort(int * arr,size_t n){
	int exp;    
    // 指数。当对数组按各位进行排序时,exp=1;按十位进行排序时,exp=10;...
    int max = get_max(arr, n);    // 数组a中的最大值
    int tmp[n];
    int count[10]; //计数器
    // 从个位开始,对数组a按"指数"进行 计数排序
    int i,j;	
    for (exp = 1; max/exp > 0; exp *= 10){
        for(i = 0; i < 10; i++) count[j] = 0; //每次分配前清空计数器
        for(i = 0; i < n; i++) 
            ++count[(arr[j] / exp) % 10]; //统计每个桶中的记录数
        for(i = 1; i < 10; i++) 
            count[j] = count[j - 1] + count[j]; //将tmp中的位置依次分配给每个桶
        for(i = n - 1; i >= 0; i--) //将所有桶中记录依次收集到tmp中
        {
            tmp[count[k] - 1] = arr[(arr[j] / exp) % 10];
            count[k]--;
        }
        for(i = 0; i < n; i++) //将临时数组的内容复制到data中
            arr[j] = tmp[j];
    }   
}

/* 	
	适用于密集且数据跨度小的序列
  	计数排序(Counting Sort)不是基于比较的排序算法,
  	其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 
  	作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
*/
void count_sort(int * arr,size_t n){
	int max = arr[0],min = arr[0];
	int i,j;
	for(i=0;i<n;++i){ // 找出待排序的数组中最大和最小的元素,
		if(arr[i]>max) max = arr[i];
		else if(arr[i]<min) min = arr[i];
	} 
	int cnt = max-min+1; // 计算出存储数据作为数组下标
	int flag[cnt]; // 存储数据出现频率的数组范围 [min,max]
	for(i=0;i<cnt;++i) flag[i] = 0; // 清零
    //计算出数据对应的下标,存入频率数组,出现1次值为1,以后++;
    //反向填充目标数组:便利频率数组,
	for(i=0;i<n;++i) ++flag[arr[i]-min];
    // 通过index反向计算出原始值(index + min),依次加入目标数组。
	for(i=0,j=0;i<cnt;++i){
		while(flag[i]>0){
			arr[j++] = i+min;
			--flag[i];
		}
	}    
}

/*
	基数排序:根据键值的每位数字来分配桶;
	计数排序:每个桶只存储单一键值;
	桶排序:每个桶存储一定范围的数值;
*/

外部排序

//每个归并段的数据量为10个 
#define NUM_OF_SEGMENT 10 

long num_of_file(const char *file){
	FILE *fp = fopen(file,"r");
	if(fp == NULL){
		return -1;
	}
	fseek(fp,0,SEEK_END);
	long fs = ftell(fp);//文件大小  字节  long    
	fclose(fp);
	return fs/4;
}

#define FILE_NAME 48
typedef struct MergeSegment{
	char file[FILE_NAME];
	size_t size;            //归并段中数据的总数
	size_t num;             //这个归并段已经读取的个数 
	FILE *fp;  
}MSM; 

//这个归并段是否处理完成了   处理完成了表示没有数据了 
bool is_completed(MSM *pms){
	return pms->size == pms->num;
} 

int load_data(MSM *pms,int data[]){
	if(is_completed(pms)){
		return 0;
	}
	int ret = fread(data,sizeof(int),NUM_OF_SEGMENT,pms->fp);
	pms->num += ret;
	return ret;
} 

int save_data(MSM *pms,int data[],size_t num){
	int ret = fwrite(data,sizeof(int),num,pms->fp);
	pms->size += ret;
	return ret;
}

void fclose_file(MSM *pms){
	fclose(pms->fp);
} 

int open_file_read(MSM *pms){
	pms->fp = fopen(pms->file,"rb");
	if(pms->fp == NULL){
		printf("open %s failed!\n",pms->file);
		return -1;
	}
	return 0;
}

int open_file_write(MSM *pms){
	pms->fp = fopen(pms->file,"wb");
	if(pms->fp == NULL){
		return -1;
	}
	return 0;
}

//init_segment(file,msms,segs);
void init_segment(const char *file,MSM *msms,int segs){
	int data[NUM_OF_SEGMENT] = {};
	int i;
	FILE *fp = fopen(file,"rb");
	for(i=0;i<segs;++i){
		int ret = fread(data,sizeof(int),NUM_OF_SEGMENT,fp);
		quick_sort(data,ret);
		//printf("ret = %d\n",ret);
		save_data(&msms[i],data,ret);
		fclose_file(&msms[i]);
	}
}

void merge(MSM *pm1,MSM *pm2,MSM *pres){
	open_file_read(pm1);
	open_file_read(pm2);
	printf("[1]原始文件数据:fs1 = %d  fn1 = %d, fs2 = %d  fn1 = %d\n",pm1->size,pm1->num,pm2->size,pm2->num);
	//strcpy(pres->file,"1");   //文件同名了 
	strcpy(pres->file,"ab");//这个地方错了   不能直接在名字前加一个1    因为  之前有0-19.txt  会提前删除掉数据 
	strcat(pres->file,pm1->file);
	pres->fp = NULL;
	pres->num = pres->size = 0;
	open_file_write(pres);
	int data1[NUM_OF_SEGMENT] = {};
	int data2[NUM_OF_SEGMENT] = {};
	int i=0,j=0;
	int cnt1 = 0,cnt2 = 0;
	cnt1 = load_data(pm1,data1);
	cnt2 = load_data(pm2,data2);
	while(i<cnt1 && j<cnt2){
		if(data1[i] < data2[j]){
			save_data(pres,&data1[i],1);
			++i;
		}else{
			save_data(pres,&data2[j],1);
			++j;
		}
		if(i==cnt1 && !is_completed(pm1)){
			cnt1 = load_data(pm1,data1);
			i = 0;
		}
		if(j==cnt2 && !is_completed(pm2)){
			cnt2 = load_data(pm2,data2);
			j = 0;
		}
	}
	while(i<cnt1){
		save_data(pres,&data1[i],cnt1-i);
		if(is_completed(pm1)){
			break;
		}
		cnt1 = load_data(pm1,data1);
		i = 0;
	}
	while(j<cnt2){
		save_data(pres,&data2[j],cnt2-j);
		if(is_completed(pm2)){
			break;
		}
		cnt2 = load_data(pm2,data2);
		j = 0;
	}
	printf("[2]原始文件数据:fs1 = %d  fn1 = %d, fs2 = %d  fn1 = %d\n",pm1->size,pm1->num,pm2->size,pm2->num);
	printf("写入文件大小  res fs=%d\n",pres->size);
}

//假设文件中存储的都是int类型   如果要写通用  传递比较函数   每一个数据的字节大小 
int outside_sort(const char *file){
	assert(file!=NULL);
	long cnt = num_of_file(file);
	if(cnt == -1){
		return -1;
	} 
	printf("%d\n",cnt);
	//归并段的个数   
	int segs = cnt/NUM_OF_SEGMENT; 
	if(cnt%NUM_OF_SEGMENT>0){
		++segs;
	}
	MSM msms[segs];
	int i;
	for(i=0;i<segs;++i){
		sprintf(msms[i].file,"%d.txt",i);
		msms[i].num = 0;
		msms[i].size = 0;
		msms[i].fp = NULL;
		open_file_write(&msms[i]);//打开文件用于写 
	}
	init_segment(file,msms,segs);
	
	while(segs>1){
		MSM res = {};
		int cnt = 0;
		for(i=0;i<segs;i+=2){
			if(i+1<segs){			
				merge(&msms[i],&msms[i+1],&res);
				fclose_file(&msms[i]);
				unlink(msms[i].file);
				fclose_file(&msms[i+1]);
				unlink(msms[i+1].file);   //删除文件
				fclose_file(&res); 
				msms[cnt++] = res;
			}else{
				msms[cnt++] = msms[i];    //剩下最后一个没有可以归并的 
			}
		}
		segs = cnt;
	}
	//msms[0];
	printf("\n----------------------\n");
	FILE *fp = fopen(msms[0].file,"rb");
	int d = 0;
	while(fread(&d,4,1,fp)>0){
		printf("%d ",d);
	}
	printf("\n");
	fclose(fp);
	
/*	
	for(i=0;i<segs;++i){
		open_file_read(&msms[i]);
		int data[NUM_OF_SEGMENT] = {};
		int ret = load_data(&msms[i],data);
		int j;
		for(j=0;j<ret;++j){
			printf("%d ",data[j]);
		}
		printf("\n");
		fclose_file(&msms[i]);
	}
*/
}
posted @ 2022-09-25 18:24  打工搬砖日记  阅读(17)  评论(0编辑  收藏  举报