[数据结构算法学习笔记]:常见排序

目录

  • **1.常见排序分类**
  • 2.具体实现
    • 2.1.插入排序
      • 2.1.1直接插入排序
      • 2.1.2希尔排序
    • 2.2选择排序
      • 2.2.1直接选择排序
      • 2.2.1堆排序
    • 2.3交换排序
      • 2.3.1冒泡排序
      • 2.3.2快速排序
        • 前后指针法:
        • 三数取中:
        • 挖坑法:
        • 左右指针法:
        • 小区间优化:
        • 拓展:快速排序的非递归写法
    • 2.4归并排序
      • 应用:(大量数据)文件排序
    • 2.5计数排序
    • 2.6基数排序

1.常见排序分类

1.插入排序:直接插入排序,希尔排序;
2.选择排序:直接选择排序,堆排序;
3.交换排序:冒泡排序,快速排序;
4.归并排序;(可以用于内存排序和外存(如硬盘)排序);
5.基数排序;
以下排序以升序为例.
注:这是初学者的笔记,理解可能存在不足,如有错误,请教我,谢谢.

2.具体实现

2.1.插入排序

2.1.1直接插入排序

基本思路:将一个数字key插入到一个有序数组中;利用一个指针(数字下标)从后向前比较,比key大,那么指针向前移动,继续向前比较,并且把该数字向后移动一位,为之后插入key留出空间,比key小,指针停止,将key插入到指针指向的后一位.
总结:时间复杂度O(N^2);空间复杂度O(1);稳定(稳定指相同数字相对顺序不发生改变);
[数据结构算法学习笔记]:常见排序_第1张图片
1到10,依次从前向后;这里插入7,key = 7,用key来保存7的数字,方便数字移动时覆盖7的位置;
[数据结构算法学习笔记]:常见排序_第2张图片
10 > 7,将10存入7的位置, 指针向前移动,继续比较,重复该过程;
[数据结构算法学习笔记]:常见排序_第3张图片
直到找到比key小的数字,将key插入到此位置;
[数据结构算法学习笔记]:常见排序_第4张图片

有序区间[0,end],待插入数在end+1位置;

	int key = arr[end + 1];
	//找到比key小的位置
	while (arr[end] > arr[end + 1] && end >= 0)
	{
		//比key大,一次后移,为插入key留出位置
		arr[end + 1] = arr[end];
		end--;
	}
	//将key插入到end下一位
	arr[end + 1] = key;

到此,完成了一次插入排序.一个完整的排序由许多次插入排序组成:
当一个无序的数字从起始位置开始,起始位置只有一个数字,可以认为是有序的,将第二位数字作为待插入数字key,插入到[0,0]这个区间,得到一个新的有序区间[0,1],重复上述,将排好序有序区间的下一位作为待插入数key,一次插入,直到整个数组有序,完成数组排序.
[数据结构算法学习笔记]:常见排序_第5张图片
下面是代码,C语言
时间复杂度O(N^2);空间复杂度O(1)

//直接插入排序	时间复杂度O(N^2);空间复杂度O(1)
void InsertSort(int* arr, int n)
{
	assert(arr);//空指针断言
	for (int i = 0; i < n - 1; i++)
	{
		int end = i;
		int key = arr[end + 1];
		//找到比key小的位置
		while (arr[end] > key && end >= 0)
		{
			//比key大,一次后移,为插入key留出位置
			arr[end + 1] = arr[end];
			end--;
		}
		//将key插入到end下一位
		arr[end + 1] = key;
	}
}
--- 分割线   | ---

2.1.2希尔排序

基本思想:
这是对直接插入排序的优化,直接插入排序如果在1,2,3,4,5,6,7这个有序区间插入0,那么前面的数据都需要依次向后挪动,效率低;希尔排序包括预排序和排序,其中预排序可以解决这个.
将一个未排序数组按照间距gap分类,将间距为gap的数据作为一组进行直接插入排序.好处:1.数组挪动不在是一个挨着一个,挪动更快;2.预排序将数组从无序转换为有一定顺序,便于后面正式排序.
总结:时间复杂度O(N^1.3) ~ O(N^2);空间复杂度O(1),不稳定.
1.分组
[数据结构算法学习笔记]:常见排序_第6张图片
2.预排序
[数据结构算法学习笔记]:常见排序_第7张图片
代码:
其实就是将直接插入排序的间距1改为gap;

		int gap = 3;
		int key = arr[end + gap];
		//找到比key小的位置
		while (arr[end] > key && end >= 0)
		{
			//比key大,一次后移,为插入key留出位置
			arr[end + gap] = arr[end];
			end -= gap;
		}
		//将key插入到end下一位
		arr[end + gap] = key;
		arrayPrintf(arr, n);

预排序完成后,正常直接插入排序.
实际排序中,间距gap并不是一个固定的值,由数组长度确定,由大到小;
当gap>1时,预排序
当最后一次gap = 1的时候,就是直接插入排序,即正式排序;

void ShellSort(int* arr, int n)
{
	int gap = n;

	while (gap > 1)
	{
		gap = gap / 3 + 1;
		//i++,而不是i += gap,这里是多组并排的思想,不是一组一组排序,而是同时进行
		for (int i = 0; i < n - gap; i++)//是n - gap,不是n - 1 -gap
		{
			int end = i;
			int key = arr[end + gap];
			//找到比key小的位置
			while (arr[end] > key && end >= 0)
			{
				//比key大,一次后移,为插入key留出位置
				arr[end + gap] = arr[end];
				end -= gap;
			}
			//将key插入到end下一位
			arr[end + gap] = key;
		}
	}
}
--- 分割线   | ---

2.2选择排序

2.2.1直接选择排序

思路:遍历数组,选出最大的,与最后一位交换,继续遍历数组,选出次大的数,与倒数第二位交换,重复上述过程,直到排序完成.
总结:时间复杂度O(N^N),空间复杂度O(1),不稳定.
[数据结构算法学习笔记]:常见排序_第8张图片

代码:

void SelectSort(int* arr, int n)
{
	assert(arr);
	int end = n - 1;
	while (end > 0)
	{
		int maxIndex = 0;//设初始最大原下标位0,即第一个元素
		//遍历数组,找出最大元素的下标
		for (int i = 0; i <= end; i++)
		{
			if (arr[i] > arr[maxIndex])
			{
				maxIndex = i;
			}
		}
		swap(&arr[maxIndex], &arr[end--]);
	}
}

交换两元素代码:

//交换
void swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
--- 分割线   | ---

2.2.1堆排序

基本思路:堆分为大堆和小堆;大堆:父亲节点大于等于子节点.小堆:父亲节点小于等于子节点;所以,根节点就是数组的最大值/最小值.
升序排列,选出最大的放在数组最后,选出次大值放在倒数第二位,重复上述过程,直到排序完成.
那么,按照这个思路,利用建立大堆选出最大值arr[0],与arr[end]交换,完成最大值的排序,将数组[0,end - 1]区间建大堆(由于只有root根节点不符合,左子树和右子树符合大堆,所以只用一次向下调整算法),这就是基本操作单元.然后将次大值arr[0]与arr[end - 1]交换,完成了次大值的排序.思路分析结束,接下来循环,直到排序完成.
总结:时间复杂度O(N*logN),空间复杂度O(1),不稳定.
注:关于堆的建立,放在二叉树的笔记章节,还没有写,之后补上.
[数据结构算法学习笔记]:常见排序_第9张图片
[数据结构算法学习笔记]:常见排序_第10张图片

代码

//向下调整算法
void adjustdown(int* arr, int root, int n)
{
	int parents = root;//父节点
	int chird = 2 * parents + 1;//左孩子
	while (parents < n)
	{
		//找左右孩子中最大者
		if (chird + 1 < n && arr[chird + 1] > arr[chird])
		{
			chird++;
		}
		//大的上浮,小的下沉
		if (chird < n && arr[chird] > arr[parents])//注意条件:chird < n
		{
			swap(&arr[chird], &arr[parents]);
			parents = chird;
			chird = 2 * parents + 1;
		}
		else
		{
			break;
		}
	}
}

//堆排序
void HeapSort(int* arr, int n)
{
	assert(arr);
	//建立堆,向下调整算法
	int chird = n - 1;
	int root = (chird - 1) / 2;
	while (root >= 0)
	{
		adjustdown(arr, root--, n);
	}
	//堆顶与末尾交换,调整最值到末尾,忽略最后一位,继续建堆
	int size = n - 1;
	while (size > 0)
	{
		swap(&arr[0], &arr[size]);
		adjustdown(arr, 0, size);
		size--;
	}
}
--- 分割线   | ---

2.3交换排序

2.3.1冒泡排序

基本思路:
这应该是大家最开始学到排序方法,最熟悉.思想概况就是第一次遍历数组把最大值依次向后交换,直到最大值在最后一位.第二次遍历数组,将次大的元素依次交换到倒数第二位…重复,直到排序完成.
总结:时间复杂度O(n^2),空间复杂度O(1),稳定.
[数据结构算法学习笔记]:常见排序_第11张图片

[数据结构算法学习笔记]:常见排序_第12张图片

//冒泡排序
void BubbleSort(int* arr, int n)
{
	assert(arr);
	int i = 0;
	int j = 0;
	//外层,遍历次数,只用遍历n - 1次,当n - 1个数排序完成,最后一个数字也就排序完成 
	for (i = 0; i < n - 1; i++)
	{
		//内层,将相对大的数字向后交换.已经排序好的位置不用比较,小心数组越界
		for (j = 0; j < n - 1 - i; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				swap(&arr[j], &arr[j + 1]);
			}
		}
	}
}
--- 分割线   | ---

2.3.2快速排序

基本思路:
在数组中选择一个基准Key,把比key大的数字放在key右边,把比key小的数字放在key左边.
这时数组区间就被分割为[0, keyIndex - 1] keyIndex [keyIndex, end],keyIndex时key的数组下标.
然后对左右区间继续分割,选出新的基准key,大的放右边,小的放左边.
不断分割,直到所有区间只有1个数字,1个数字可以认为是有序的,此时,排序完成.
总结:时间复杂度O(N*logN),空间复杂度O(logN)(递归的深度),不稳定(三数取中的交换).
[数据结构算法学习笔记]:常见排序_第13张图片
为了逻辑清晰,单次排序分离出来单独作为函数.
使用前后指针法的单次排序思路:

前后指针法:

[数据结构算法学习笔记]:常见排序_第14张图片

//快速排序 单次排序
//前后指针法
int SortPart1(int* arr, int begin, int end)
{
	//选择基准key,以end为基准
	int key = arr[end];
	int prev = begin - 1;
	int cur = begin;
	//目的:比key大的放左边,比key小的放左边
	// [小于key区间]  key  [大于key区间]
	while (cur <= end)
	{
		//cur遇到比key小的值就停下,prev++,然后交换cur与prev指向的数字
		//cur后面连续跟着大于key的数字,prev后面跟着小于等于key的数字
		//最后一次交换keyIndex指向的key,所以遍历完成后prev指向数字是key
		//就像两个火车头:    小于等于key...(key)prev  大于key...cur 
		while (arr[cur] > key && cur <= end)
		{
			cur++;
		}
		if (arr[cur] <= key && cur <= end)
		{
			swap(&arr[++prev], &arr[cur]);
			cur++;
		}
	}
	return prev;
}

//快速排序
void QuickSort(int* arr, int begin ,int end)
{
	assert(arr);
	if (begin >= end)//结束递归条件
	{
		return;
	}

	int div = SortPart1(arr, begin ,end);
	//[0, div -1] div [div + 1,end]
	//递归,继续分割排序左右区间,结构类似二叉树,前序
	QuickSort(arr, begin, div - 1);
	QuickSort(arr, div + 1, end);
}
三数取中:

由于切分区间的基准key是数组的最后一个,当整个数组都有序(逆序)时,区间分割变得低效,左边[0,n-2],右边[n-1];
程序的运行次数变多,时间复杂度增大,为了解决这个问题,需要选择一个相对中间大小的数作为基准key,这里用到一个三数取中函数:获取数组起始位置,中间位置,末尾三个数中不大不小的值.
三数取中代码:

//三数取中
int GetMidNumber(int* arr, int begin, int end)
{
	int left = begin;
	int mid = (begin + end) / 2;
	int right = end;

	if (arr[left] > arr[mid])
	{
		if (arr[mid] > arr[right])
		{
			return mid;
		}
		else if(arr[right] > arr[left])
		{
			return left;
		}
		else
		{
			return right;
		}
	}

	else // mid => left
	{
		if (arr[right] > arr[mid])
		{
			return mid;
		}
		else if (arr[left] > arr[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}

接下来,在单次排序函数SortPart1()的最开始加上以下代码,完成三数取中,提高效率.

	//三数取中,将相对中间大小的数放在最右边,让key取为基准
	int mid = GetMidNumber(arr, begin, end);
	swap(&arr[mid], &arr[end]);

当然,单次排序方法不止一种,还有挖坑法,左右指针法.

挖坑法:

[数据结构算法学习笔记]:常见排序_第15张图片
[数据结构算法学习笔记]:常见排序_第16张图片

//快速排序
//单次循环,挖坑法
int SortPart1(int* arr, int begin, int end)
{
	//三数取中,将相对中间大小的数放在最右边,让key取为基准
	int mid = GetMidNumber(arr, begin, end);
	swap(&arr[mid], &arr[end]);

	assert(arr);
	//选最右边为key
	int key = arr[end];
	int tmp = end;	//挖坑,初始坑

	while (begin < end)
	{
		//选右边为初始坑,从左边开始找
		//从左边开始,找到大于等于key的值
		while (arr[begin] <= key && begin < end)
		{
			begin++;
		}
		if (arr[begin] > key && begin < tmp)//&& begin < tmp
		{
			arr[tmp] = arr[begin];//填坑
			tmp = begin;		  //新挖的坑
			begin++;
		}

		while (arr[end] >= key && begin < end)
		{
			end--;
		}
		if (arr[end] < key && tmp < end)//&& tmp < end; 最后begin和end同时指向的数小于key,可以交换的情况
		{
			arr[tmp] = arr[end];
			tmp = end;
			end--;
		}
	}

	//key填到最后的坑
	arr[tmp] = key;
	return tmp;
}
左右指针法:

[数据结构算法学习笔记]:常见排序_第17张图片

//快速排序
//单次循环,左右指针法
int SortPart3(int* arr, int begin, int end)
{
	//基准定在右边
	//左指针找到比key大的时候停下,右指针找比key小的时候停下
	//然后交换左右指针指向数值
	//左指针先走,这样最后指针相遇位置的数值比key大,正好可以与key交换

	//三数取中,避免最坏情况,逆序
	int mid =  GetMidNumber(arr, begin, end);
	swap(&arr[mid], &arr[end]);
	int key = arr[end];
	int left = begin;
	int right = end - 1;
	// left ... right key

	while (left < right)//left < right;别写错,不是left < end
	{
		//左指针找大
		while (arr[left] <= key && left < right)//等于key的情况不用管,之后区间细分的时候等于的就变最大值,会升序
		{
			left++;
		}
		//右指针找小
		while (arr[right] >= key && left < right)
		{
			right--;
		}
		//交换
		if (arr[left] > key && arr[right] < key)
		{
			swap(&arr[left], &arr[right]);
			//left++;
			//right--;
		}
	}
	//交换key到左右指针相遇处
	swap(&arr[left], &arr[end]);

	return left;
}

上面的三种单次排序方法在写代码前最好先画图确定思路,明确细节,不然很容易出错.

小区间优化:

当然,除了三数取中的优化,还有小区间优化,当区间分割得比较小时,继续分割性价比没有那么高,(递归深度增加,函数调用增加,需要排序数字相对少),所以小区间可以直接排序,不在分割.
优化后的快速排序:

//快速排序
//选定一个基准值key,比key小的放在左边,比key大的放在右边
//然后排序左边和右边,就像二叉树
//直到不可再分割
// 1.0	时间复杂度O(N*logN)		空间复杂度O(logN),递归的深度
void QuickSort(int* arr, int begin, int end)
{
	assert(arr);
	//递归结束条件,区间不能再分
	if (begin >= end)
	{
		return;
	}

	//小区间优化,减少递归深度
	//排序[arr + begin ,arr + end]区间的数字
	if (end - begin + 1 < 10)
	{
		//直接插入排序
		InsertSort(arr + begin, end - begin + 1);//注意:这里的排序是从arr + begin开始,不能是arr,arr一直不变
		return;
	}

	//递归,排序;排序左半边和右半边
	int div = SortPart3(arr, begin, end);
	QuickSort(arr, begin, div - 1);
	QuickSort(arr, div + 1, end);
}

当然,单次排序也可以放在快速排序函数内部,单次排序单独出来的目的主要还是为了逻辑清晰,简单.

//2.0	快速排序,减少子函数版本
void QuickSort(int* arr, int begin, int end)
{
	assert(arr);
	//递归结束条件,区间不能再分
	if (begin >= end)
	{
		return;
	}

	//递归,排序;排序左半边和右半边
	int mid = GetMidNumber(arr, begin, end);
	swap(&arr[mid], &arr[end]);
	//选最右边为key
	int key = arr[end];
	int tmp = end;	//挖坑,初始坑
	int left = begin, right = end;

	while (left < right)
	{
		//选右边为初始坑,从左边开始找
		//从左边开始,找到大于等于key的值
		while (arr[left] <= key && left < right)
		{
			left++;
		}
		if (arr[left] > key)
		{
			arr[tmp] = arr[left];//填坑
			tmp = left;		  //新挖的坑
			left++;
		}

		while (arr[right] >= key && left < right)
		{
			right--;
		}
		if (arr[right] < key)
		{
			arr[tmp] = arr[right];
			tmp = right;
			right--;
		}
	}
	arr[tmp] = key;

	int div = tmp;
	QuickSort(arr, begin, div - 1);
	QuickSort(arr, div + 1, end);
}
拓展:快速排序的非递归写法

递归主要用到了栈的后进先出性质保存变量,非递归写法就需要自己实现一个数据结构栈(C语言没有提供数据结构栈,所以需要自己写),将分割后的区间[begin,end]两个端点下标存入栈,利用循环模拟实现递归.
代码实现:

//3.0 快速排序(非递归)	2.0
void QuickSortNonR(int* arr, int begin, int end)
{
	assert(arr);
	//assert(begin < end);

	stack s;
	StackInit(&s);
	//[begin,end]
	StackPush(&s, end);
	StackPush(&s, begin);
	
	while (!StackEmpty(&s))
	{
		int left = StackTop(&s);
		StackPop(&s);
		int right = StackTop(&s);
		StackPop(&s);

		//利用数据库栈模拟递归,排序;排序左半边和右半边
		int div = SortPart1(arr, left, right);
		//[left , div - 1] div [div + 1,right]
		if (div + 1 < right)
		{
			StackPush(&s, right);
			StackPush(&s, div + 1);
		}
		if (left < div - 1)
		{
			StackPush(&s, div - 1);
			StackPush(&s, left);
		}
	}

	//别忘了free栈
	StackDestroy(&s);
}

栈的接口函数按照你自己实现的方法调用.

--- 分割线   | ---

2.4归并排序

基本思路:
从两个有序数组中依次选出较小值归并到新的数组,当两个数组遍历完成,得到的新数组就是两个数组结合后的升序排列.
由于通常待排序的数组不是有序数组,所以可以采用分治的思想,把问题拆分,连续的数值并不有序,那么就把数值拆分,例如:将1个带有n个元素的数组(1个[0, n-1]的区间.),拆分为n个只有一个元素的数组(n个[0]的区间).后者两两就可以归并为有序数组.
总结:空间换取时间,外排序,空间复杂度O(N),时间复杂度O(N*logN),稳定;
思路图:
[数据结构算法学习笔记]:常见排序_第18张图片
[数据结构算法学习笔记]:常见排序_第19张图片

将两个数组归并排序,即从两个数组中选出较小值放入一个临时空间tmp,归并排序好后再将tmp中有序的数组拷贝回原数组arr,为下一次归并做基础.
递归拆分后的逻辑单元是 ==> 从arr中两个有序的数组中抽取两个数字依次放入tmp排序.排序好有序的数组是arr接下来继续归并到tmp的基础,所以需要拷贝tmp到arr.
[数据结构算法学习笔记]:常见排序_第20张图片
代码,因为需要申请空间,母程序不适合递归,所以这里用到一个子程序来递归.
(当然,也可以设置一个flag的静态变量用于第一次申请空间的标志,然后改变flag值,之后不再申请空间,排序完成后再初始化flag值,不过这样操作似乎比较麻烦,所以没有使用)

从上图可以看到,归并排序由几个步骤:
1.将一个无序数组拆分,直到不可拆解(只有一个元素)
2.将两个有序的数组归并排序到临时空间tmp
3.将tmp排序好的数组拷贝回原数组arr对应位置
小单元归并成大单员,大单元继续归并,直到排序完成

//归并排序
void MergeSort(int* arr, int begin, int end)
{
	//临时空间存储归并后数据
	int* tmp = (int*)malloc(sizeof(int) * (end - begin + 1));
	if (NULL == tmp)
	{
		perror("malloc error");
		exit(-1);
	}

	//1.拆分 2.归并
	//begin end
	_MergeSort(arr, tmp, begin, end);

	//释放tmp
	free(tmp);
	tmp = NULL;
}

用于递归的子程序:

//代码优化 1.0
//归并排序子程序
void _MergeSort(int* arr, int* tmp, int begin, int end)
{
	assert(arr);
	if (begin >= end)//结束递归条件
	{
		return;
	}

	//二叉树的前序遍历 ==> 拆分
	//[begin end] ==>  [begin1 end1] [begin2 end2]
	int begin1 = begin;
	int end1 = (begin + end) / 2;
	int begin2 = end1 + 1;       //小细节:与if (begin >= end)相对应
	int end2 = end;
	_MergeSort(arr, tmp, begin1, end1);
	_MergeSort(arr, tmp, begin2, end2);

	//[begin1 end1] [begin2 end2]
	//归并+排序
	int tmpbegin = begin;
	//两个有序数组中选出较小值归并到tmp数组
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (arr[begin1] < arr[begin2])
		{
			tmp[tmpbegin++] = arr[begin1++];
		}
		else
		{
			tmp[tmpbegin++] = arr[begin2++];
		}
	}

	//当其中一组结束,另一组处理
	while (begin1 <= end1)
	{
		tmp[tmpbegin++] = arr[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[tmpbegin++] = arr[begin2++];
	}

	//将排序好的tmp数组拷贝覆盖arr
	// 因为递归拆分后的逻辑单元是 ==> 从arr中两个有序的数组中抽取两个数字依次放入tmp排序
	// 排序好有序的数组是arr接下来继续归并到tmp的基础
	//把每次归并后的数据放回原数组 ==> 关键:别忘了!
	int i = begin;
	for (i = begin; i <= end; i++)
	{
		arr[i] = tmp[i];
	}
}

不同点:归并排序与上述排序有一点不同,归并排序可以作为外排序,再外存(硬盘)上排序.上述排序被称为内排序,只能再内存上排序.
例如,大量数据排序(超过内存存储最大值).

应用:(大量数据)文件排序

思路:
1.将一个大文件拆分为多个能存入内存的小文件
2.小文件在内存中排序
3.读取排序后的小文件,两两归并排序到一个新建文件tmp
4.当所有小文件归并完成,最后生成的一个tmp文件就是排序后结果
注意:
1.fscanf()的返回值判断,EOF.int ret = fscanf( , ,&num);
2.文件操作文件指针自动前移的处理
3.一边读取,一边写入(看起来)
[数据结构算法学习笔记]:常见排序_第21张图片
代码:

//文件归并排序子程序:从fout1,fout2文件读取数据,一边读取一边顺序写入tmp文件
void _OutMergeSort(char* fout1, char* fout2, char* tmp)
{
	FILE* f1 = fopen(fout1, "r");
	FILE* f2 = fopen(fout2, "r");
	FILE* f3 = fopen(tmp, "w");

	//因为文件指针自动向前,不能直接用fscanf()的返回值判断文件是否结束
	// (直接用可能导致文件指针前移,但上次读取数据还未使用就被覆盖)
	//将fscanf()的返回值存入ret,利用ret判断,能有效控制文件指针移动
	//解决一个文件中数据都比另一个文件小,文件指针不好控制问题
	int num1, num2;
	int ret1 = fscanf(f1, "%d\n", &num1);
	int ret2 = fscanf(f2, "%d\n", &num2);
	while (ret1 != EOF && ret2 != EOF)
	{
		if (num1 < num2)
		{
			fprintf(f3, "%d\n", num1);
			ret1 = fscanf(f1, "%d\n", &num1);
		}
		else
		{
			fprintf(f3, "%d\n", num2);
			ret2 = fscanf(f2, "%d\n", &num2);
		}
	}

	//另一个文件剩下的数据
	while (ret1 != EOF)
	{
		fprintf(f3, "%d\n", num1);
		ret1 = fscanf(f1, "%d\n", &num1);
	}
	while (ret2 != EOF)
	{
		fprintf(f3, "%d\n", num2);
		ret2 = fscanf(f2, "%d\n", &num2);
	}

	fclose(f1);
	fclose(f2);
	fclose(f3);
}


//(大)文件排序
void OutMergeSort(FILE* pf)
{
	//拆分文件,直到内存能容纳
	//假设拆分为10份
	int arr[100] = { 0 };//拆分后存入内存
	int i = 0;
	int n = 100; //分割后的每个文件存储n=100个数据
	char* fname[20];//分割后文件名
	int fileid = 1;	//分割后文件id序号

	while (fscanf(pf, "%d\n", &arr[i]) != EOF)
	{
		if (i < n - 1)//[0 ~ n-2] 共n-1个数字;解决最后一个数据的存储问题
		{					//(最后一次判断fscanf()!= EOF又存入了一个数)		
			i++;
		}
		else
		{
			i++;  //i == n
			//将分割后的文件排序:快速排序
			QuickSort(arr, 0, n - 1);
			//创建文件存储分割后的数据
			sprintf(fname, "%d", fileid++);
			//创建文件:打开,写入,关闭
			FILE* fin = fopen(fname, "w");
			for (int j = 0; j < i; j++)
			{
				fprintf(fin, "%d\n", arr[j]);
			}
			fclose(fin);

			//初始化i
			i = 0;
		}
	}

	//当最后数据不足n个时:
	//这里不用i++;最后一次读取似乎无意义,==EOF是情况,越界
	sprintf(fname, "%d", fileid++);
	QuickSort(arr, 0, i - 1);
	FILE* fin = fopen(fname, "w");
	for (int j = 0; j < i; j++)
	{
		fprintf(fin, "%d\n", arr[j]);
	}
	fclose(fin);

	//归并排序
	//将1,2文件归并为tmp
	//直到归并到最后一个文件
	char* fout1[20];	//文件名
	char* fout2[20];
	char* tmp[20];
	sprintf(fout1, "%d", 1);
	sprintf(fout2, "%d", 2);
	sprintf(tmp, "1_%d.tmp", 2);

	FILE* f1 = fopen(fout1, "r");	//文件指针名字
	FILE* f2 = fopen(fout2, "r");
	FILE* f3 = fopen(tmp, "w");		//写
	_OutMergeSort(fout1, fout2, tmp);//归并排序
	fclose(f1);
	fclose(f2);
	fclose(f3);
	f1 = f2 = f3 = NULL;

	//依次归并,直到分割的最后一个文件
	for (int i = 3; i < fileid; i++)
	{
		sprintf(fout1, tmp);	//将上一次归并后的文件作为这一次归并的两个文件之一
		sprintf(fout2, "%d", i);
		sprintf(tmp, "1_%d.tmp", i);//新创建一个文件,用于存放归并后的数据
		_OutMergeSort(fout1, fout2, tmp);//归并排序
	}
}

为了方便测试,添加了生成数据函数,和主函数:

//生成一个大量数据文件
void CreatFile()
{
	//打开文件
	FILE* pf = fopen("BigData", "w");

	//写入
	for (int i = 1005; i >= 0; i--)
	{
		fprintf(pf, "%d\n", i);
	}
	//关闭文件
	fclose(pf);
	pf = NULL;
}
int main()
{
	//生成一个大量数据文件
	CreatFile();

	//打开文件
	FILE* pf = fopen("BigData", "r");

	//排序文件
	OutMergeSort(pf);

	//关闭文件
	fclose(pf);
	pf = NULL;

	return 0;
}
--- 分割线   | ---

2.5计数排序

思路:
统计一个范围从min到max的数组每个数字出现的次数,出现的次数保存在一个范围num[0,Range]的新数组;其中min出现次数保存在num[0],max出现次数保存在num[Range],一一对应;num[i]记录了min+i的数字出现次数,将min+i按照出现的次数写入原数组,即可排序完成.
//只适用整数,范围集中
总结:时间复杂度O(N + Range); 空间复杂度O(Range);不稳定;
[数据结构算法学习笔记]:常见排序_第22张图片

代码:

//只适用整数,范围集中
//时间复杂度O(N + Range); 空间复杂度O(Range);
void CountSort(int* a, int n)
{
	//遍历,找出min,max
	int min = a[0];
	int max = a[0];
	for (int i = 0; i < n; i++)
	{
		if (a[i] < min)
		{
			min = a[i];
		}
		if (a[i] > max)
		{
			max = a[i];
		}
	}
	//范围
	int range = max - min + 1;
	int* num = (int*)calloc(range, sizeof(int));	//初始化0
	//计数
	for (int i = 0; i < n; i++)
	{
		num[a[i] - min]++;		//记录min+i的数字出现次数
	}

	//排序
	int k = 0;
	for (int i = 0; i < range; i++)
	{
		while (num[i]-- > 0)	//num[i]记录了min+i的数字出现次数
		{
			a[k++] = min + i;	
		}
	}
}
--- 分割线   | ---

2.6基数排序

待续,未完成…

你可能感兴趣的:(数据结构算法学习笔记,算法,数据结构,学习,排序算法)