排序(C语言版)

本文详细介绍了直接插入排序、希尔排序、选择排序、堆排序、冒泡排序、快速排序(包括Hoare版本)以及归并排序的基本原理、代码实现和特性总结,重点关注它们的时间复杂度和稳定性。
摘要由CSDN通过智能技术生成

排序的重要性不言而喻,让我们看看一些常见的排序,这里我们都以升序为例:

一、直接插入排序

当插入第i(i>=1)个元素时,前面的array[0],array[1],.,array[i-1]已经排好序,此时用array[i的排序码与array[i-1]array[i-2].的排序码顺序进行比较,找到插入位置即将arrayU插入,原来位置上的元素顺序后移.

代码及注释如下:

void Insertsort(int* arr, int n)//arr数组,n元素个数
{
	for (int i = 0; i < n-1; i++)//循环n次
	{
		int end = i;//将i的值赋给end,方便将其数值改变而不影响循环
		int tmp = arr[end + 1];//由于升序,我们要提前保存要排序的数
		//这里默认前i个数是已排好的,即end
		while (end >= 0)
		{
			if (arr[end] > tmp)//arr[end]与arr[end+1]比较
			{
				arr[end + 1] = arr[end];//满足条件赋值
				end--;//继续向前排列
			}
			else
			{
				break;//不满足条件退出循环
			}
		}
		arr[end + 1] = tmp;//将保存的数值赋给留出的位置
	}
}

可以用以下代码测试:

void Print(int* arr, int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", arr[i]);
	}
}
int main()
{
	int arr[] = { 2,4,6,8,0,1,3,5,7,9 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	Insertsort(arr, sz);
	Print(arr, sz);
}

结果也显而易见:

直接插入排序是通过进行比较来插入的,最坏的情况就是都要比较,所以是O(N^2),最好情况就是本生就是顺序且有序的。

直接插入排序的特性总结:

1. 元素集合越接近有序,直接插入排序算法的时间效率越高

2. 时间复杂度:O(N^2)

3. 空间复杂度:O(1),它是一种稳定的排序算法

4. 稳定性:稳定

二、希尔排序

希尔排序大家现在当下只需知道大概在O(1.3N)左右即可

希尔排序的特性总结:
1. 希尔排序是对直接插入排序的优化。
2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就 会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的比。
3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些书中给出的希尔排序的时间复杂度都不固定,大家就记为 O(1.3N)
4. 稳定性:不稳定

代码如下:


void Shellsort(int* arr, int n)
{
	int gap = n;//将n的值赋值一份给gap,便于后续对gap给值划分
	while (gap > 1)
	{
		//法一:gap/2为单位
		gap = gap / 2;//gap的值以二分之一不断划分,最后得到gap=1进行插入排序
		//gap/3为单位
		//gap = gap / 3 + 1;//gap的值以三分之一不断划分,最后加+1得到gap=1进行插入排序
		//gap>1时进行预排序
		//gap=1时进行插入排序
		for (int i = 0; i < n - gap; i++)//i<n-gap:把间隔为gap的多组数据同时排
		{
			//下面操作和插入排序大体相同,但注意不再时加减1;而是以gap为单位!!!
			int end = i;
			int tmp = arr[end + gap];
			while (end >= 0)
			{
				if (arr[end] > tmp)
				{
					arr[end + gap] = arr[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			arr[end + gap] = tmp;
		}
	}
}

大家也可以用我插入排序时的代码进行测试,结果也是一样的。

三、选择排序

选择排序是在每一趟的遍历中找到最大的和最小的数,分别与数组末尾和开始的数据进行交换,直到数组有序。

思路比较简单,我们来一起看看代码:

void SelectSort(int* a, int n)
{
	int begin = 0;//给定初始位置
	int end = n - 1;//给定末位置
	while (begin < end)//判断循环结束条件
	{
        //开始时最大值,最小值都为第一个数据的下标,然后开始比较
		int mini = begin;
		int maxi = begin;
		for (int i = begin + 1; i <= end; i++) //从第二个数据开始找
		{
			if (a[i] < a[mini])
			{
				mini = i; //小于就交换该位置与mini的下标
			}
			if (a[i] > a[maxi])
			{
				maxi = i;//大于就交换该位置与maxi的下标
			}
		}
		Swap(&a[begin], &a[mini]); //进行交换
		if (maxi == begin) //防止重复交换导致结果不对
		{
			maxi = mini;
		}
		Swap(&a[end], &a[maxi]);
		begin++;
		end--;
	}
}

直接选择排序的特性总结:

1. 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用

2. 时间复杂度:O(N^2)

3. 空间复杂度:O(1)

4. 稳定性:不稳定

四、堆排序

堆排序即利用堆的思想来进行排序,首先要建堆

升序:建大堆

降序:建小堆

代码如下:

void Swap(int* a, int* b)
{
	int t = *a;
	*a = *b;
	*b = t;
}

void AdjustDwon(int* a, int n, int parent)
{
	int child = parent * 2 + 1;//得出左孩子下标
	while (child < n)
	{
        //如果右孩子大于左孩子,++得到右孩子
		if (child + 1 < n && a[child] < a[child + 1])
		{
			child++;
		}  
		if (a[parent] < a[child])//比较,孩子大于父亲就交换
		{
			Swap(&a[parent], &a[child]);
            //以孩子的下标为父亲进行新一轮的比较
			parent = child; 
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

void HeapSort(int* a, int n)
{
    //将数组a直接建堆,此时最大的数在堆顶
	for (int i = (n - 1 - 1) / 2; i >= 0; i++)
	{
		AdjustDwon(a, n, i);
	}
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]); //交换堆顶和末尾的数,使最大的数到最后
		AdjustDwon(a, end, 0); //将剩下的n-1个数继续建堆,不影响已经选出的最大数
		end--;
	}
}

堆排序的特性总结:

1. 堆排序使用堆来选数,效率就高了很多。

2. 时间复杂度:O(N*logN)

3. 空间复杂度:O(1)

4. 稳定性:不稳定

五、冒泡排序

这是一个可以说是最简单的排序了,实现的关键就是想好两层循环的条件就行了,它只具有教学意义,因为时间复杂度太高了。

代码如下:

void Swap(int* a, int* b)
{
	int t = *a;
	*a = *b;
	*b = t;
}

void BubbleSort(int* a, int n)
{
	for (int j = 0; j < n - 1; j++)
	{
		int flag = 1; //如果发生交换,flag=0,如果这次没发生交换,说明已经有序
		for (int i = 0; i < n - 1 - j; i++) //每排好一次,里面循环就少排一次,所以是n-1-j
		{
			if (a[i] > a[i + 1])
			{
				Swap(&a[i], &a[i + 1]);
				flag = 0;
			}
		}
		if (flag)
			break;
	}
}

1、时间复杂度:O(N^2)

2、稳定性:稳定

六、快速排序

快速排序是 Hoare 于 1962 年提出的一种二叉树结构的交换排序方法,其基本思想为: 任取待排序元素序列中 的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右 子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
先学hoare版本

注意:hoare的版本是将key取开头的元素

如果我们按照升序排列,我们要先走右边的,这样才能保证相遇的点其值一定比key点的小,原因如下:

相遇分为以下两种情况:

1.左边相遇右边,即右边停止后左边一直走,没有找到比key大的元素直到相遇,而相遇点是右边找小找到的,说明相遇点比key点小(升序)

2.右边相遇左边,即左边停止后右边一直走,没有找到比key小的元素直到相遇,而相遇点是左边找大找到的,说明相遇点比key点大。(降序)

代码如下:

void Swap(int* a, int* b)
{
	int t = *a;
	*a = *b;
	*b = t;
}
//三数取中,得到中间大的数
int GedMidi(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else if (a[right] < a[left])
		{
			return left;
		}
		else
			return right;
	}
	else //a[mid] < a[left]
	{
		if (a[left] < a[right])
		{
			return left;
		}
		else if (a[right] < a[mid])
		{
			return mid;
		}
		else
			return right;
	}
}

void QuickSort(int* a, int left, int right)//注意end到底是啥?如果是元素个数,下面的right要减1,如果是最后一个元素下标,end=right
{
    //递归结束条件
	if (left >= right)
		return;
	int begin = left, end = right;

	//三数取中间值法,避免在已经有序的情况下快排过慢的情况
	//int midi = GedMidi(a, left, right);
	//Swap(&a[left], &a[midi]);

	//或用随机数法,避免在已经有序的情况下快排过慢的情况,让随机数作keyi
	//int randi = rand() % (right - left + 1);
	//randi += left;
	//Swap(&a[left], &a[randi]);
    
    //上面的方法只是优化,可以先不看

	int keyi = left;
	while (left < right)
	{
		while (left < right && a[right] >= a[keyi]) //右找小
		{
			right--;
		}
		while (left < right && a[left] <= a[keyi]) //左找大
		{
			left++;
		}
		Swap(&a[left], &a[right]);
	}
    //相遇点和key交换
	Swap(&a[keyi], &a[left]);
    //第一次完成
	//下面是递归部分
	keyi = left;
	Quicksort(arr, begin, key-1);
	Quicksort(arr, key + 1,end)
}

其实快速排序还有两种方法,一种是挖坑法,它和上面的方法思路差不多,两种会一个即可

void Swap(int* a, int* b)
{
	int t = *a;
	*a = *b;
	*b = t;
}
//挖坑法
void partQuicksort(int* a, int begin, int end)//end表示最后一个元素下标
{
	if (begin >= end)
		return;
	int left = begin;
	int right = end;
	int key = a[begin];
	int hole = begin;//坑
	while (left < right)
	{
		while (left < right && a[right] >= key)
			right--;
		a[hole] = a[right];
		hole = right;
		while (left < right && a[left] <= key)
			left++;
		a[hole] = a[left];
		hole = left;
	}
	a[hole] = key;
	partQuicksort(a, begin, hole - 1);
	partQuicksort(a, hole + 1, end);
}

另一种是前后指针版本

void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;
	int keyi = left;
    int prev = left;
    int cur = left + 1;
    while (cur <= right)
    {
	    if (a[cur] < a[keyi] && ++prev != cur)
	    Swap(&a[prev], &a[cur]);

	    ++cur;
    }
    Swap(&a[keyi], &a[prev]);
    keyi = prev;
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}

到目前为止,大家是不是觉得快排也就那样?现在我请你实现快排的非递归,请问你如何实现呢?

// 快速排序 非递归实现
void QuickSortNonR(int* a, int left, int right)
{
	Stack st;
	StackInit(&st);//初始化栈
	//先入右,再入左
	StackPush(&st, right);//入栈
	StackPush(&st, left);

	while (!StackEmpty(&st))
	{
		int begin = StackTop(&st);//取栈顶
		StackPop(&st);//删除栈顶元素

		int end = StackTop(&st);
		StackPop(&st);

		//单趟
		int keyi = left;
		int prev = left;
		int cur = left + 1;
		while (cur <= end)
		{
			if (a[cur] < a[keyi] && ++prev != cur)
				Swap(&a[cur], &a[prev]);
			++cur;
		}
		Swap(&a[keyi], &a[prev]);
		keyi = prev;
		//先入右,再入左
		//判断是否要入栈
		if (keyi + 1 < end)
		{
			StackPush(&st, end);
			StackPush(&st, keyi + 1);
		}
		if (keyi - 1 > begin)
		{
			StackPush(&st, keyi - 1);
			StackPush(&st, begin);
		}
	}
	StackDestroy(&st);//销毁栈
}

快速排序的特性总结:

1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序

2. 时间复杂度:O(N*logN)

3. 空间复杂度:O(logN)

4. 稳定性:不稳定

七、归并排序

基本思想:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

void _MergeSort(int* a, int left, int right, int* tmp)
{
    //结束条件判断
	if (left == right)
	{
		return;
	}
	int mid = (left + right) / 2;

	_MergeSort(a, left, mid, tmp);
	_MergeSort(a, mid + 1, right, tmp);

	//归并
	int begin1 = left, end1 = mid;
	int begin2 = mid + 1, end2 = right;
	int i = left;
	//比较,小的尾插tmp
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] <= a[begin2])
		{
			tmp[i++] = a[begin1++];
		}
		else
		{
			tmp[i++] = a[begin2++];
		}
	}
	while (begin1 <= end1)
	{
		tmp[i++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[i++] = a[begin2++];
	}

	memcpy(a+left, tmp+left, sizeof(int) * (right - left + 1));
}

void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}

	_MergeSort(a, 0, n - 1, tmp);

	free(tmp);
	tmp = NULL;
}

归并排序的特性总结:

1. 时间复杂度:O(N*logN)

2. 空间复杂度:O(N*logN)

3. 稳定性:稳定

以上便是本篇博客的全部内容,感谢观看。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值