【数据结构】数据结构之排序算法

本文详细介绍了数据结构中的排序算法,包括插入排序、希尔排序、冒泡排序、快速排序、选择排序和堆排序。讨论了它们的时间复杂度、空间复杂度以及稳定性,并提供了相应的代码示例。最后提到了归并排序及其非递归实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


数据结构的各种排序算法


所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小递增或递减的

排列起来的操作。排序算法,就是如何使得记录按照要求排列的方法。


排序算法可以分为稳定排序和不稳定排序

比如对一个数组,如果A[i] = A[j],A[i]原来在位置前,排序后A[i]还是要在A[j]位置前,这样的排序叫稳定排序。


下面是几种常见的排序算法:

一、插入类排序

1.插入排序

将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据,算法适用于少量数据的

排序,时间复杂度为O(n^2)。是稳定的排序方法。

时间复杂度:T(n) = O(n²)

空间复杂度:S(n) = O(1)

给定一个乱序的数组,可以从前往后慢慢循环进行插入排序,代码如下:

void InsertionSort(int arr[], int n) {
	assert(arr);
	int i = 0;
	int j = 0;
	int tmp = 0;
	for (i = 1; i < n; ++i) {
		/*
		**从待插入数组中取出第一个元素
		**i - 1是有序数组中的最后一个元素
		*/
		tmp = arr[i];
		j = i - 1;
		/*
		** j >= 0是对有序部分的边界进行限制
		** tmp < arr[j]是插入判断条件
		*/
		while (j >= 0 && tmp < arr[j]) {
			/*
			**向后挪一位
			*/
			arr[j + 1] = arr[j];
			j--;
		}
		/*
		**在这里进行插入,找到了插入位置,下标为j + 1
		*/
		arr[j + 1] = tmp;
	}
}

2.希尔排序

希尔排序(Shell Sort)是插入排序的一种。也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本。

把待排序序列分成若干较小的子序列,然后逐个使用直接插入排序法排序,最后再对一个较为有序的序列进

行一次排序,主要是为了减少移动的次数,提高效率。原理应该就是从无序到渐渐有序,要比直接从无序到

有序移动的次数会少一些。

时间复杂度:O(n ^ 1.5)

空间复杂度:O(1)

希尔排序是非稳定排序算法。

代码如下:

void ShellSort(int arr[], int size) {
	assert(arr);
	// 1.先生成步长序列,此序列直接按照希尔序列生成
	// N / 2, N / 4, N / 8....
	int gap = size / 2;
	for (; gap > 0; gap /= 2) {
		//第一重循环,生成了步长序列
		int bound = gap;
		for (; bound < size; ++bound) {
			//第二重循环,完成所有组的元素的插入
			//先处理第一组的第一个元素...
			//再处理第二组的第一个元素..
			//...
			//再处理第一组的第二个元素
			int value = arr[bound];
			int i = bound;
			//第三重循环是对,当前元素在组内进行插入排序
			for (; i >= gap; i -= gap) {
				if (arr[i - gap] > value) {
					arr[i] = arr[i - gap];
				}
				else {
					break;
				}
			}
			arr[i] = value;
		}//bound >= size, 第一次分组和插排完成
	}//gap <= 0,整个排序结束
}


二、交换类排序

1.冒泡排序

冒泡排序(Bubble Sort),是一种 计算机科学领域的较简单的 排序算法。

它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。

走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。

时间复杂度:T(n) = O(n²)

空间复杂度:S(n) = O(1)

是一种稳定性排序

代码如下:

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

/*对一个整型数组进行冒泡排序*/
void BubbleSort(int arr[], int sz) {
	assert(arr);
	/*
	**冒泡排序是一种比较简单的排序算法
	**重复的遍历数组,每次比较两个元素,把最大或者最小的冒到最后或者最前面
	**这样一直,知道再也没有数需要进行交换,就排序完成
	*/
	int i = 0;
	/*
	**这里只需要进行sz-2次的循环,因为最后剩一个元素了就不需要再比较了
	*/
	for (; i < sz - 1; ++i) {
		int j = 0;
		for (; j < sz - i - 1; ++j) {
			/*
			**这里也一样,因为是比较当前元素和下一个元素j + 1
			*/
			if (arr[j] > arr[j + 1]) {
				Swap(&arr[j], &arr[j + 1]);
			}//arr[j] > arr[j + 1]
		}//第一遍冒泡完成,数组最后一个元素元素是最大的了
	}//冒泡完成
}

2.快速排序

冒泡排序一次只能消除一个逆序,为了能一次消除多个逆序,采用快速排序。以一个关键字为轴,从左从

右依次与其进行对比,然后交换,第一趟结束后,可以把序列分为两个子序列,然后再分段进行快速排序,

达到高效。

时间复杂度:平均T(n) = O(nn),最坏O(n²)

 空间复杂度:S(n) = O(n)

 是不稳定排序

快速排序是一种比较重要的排序方法,我这里写了三种方法:

①交换法

找到一个基准值key,然后用left和right分别从左右两边寻找,从左往右找到大于key值的元素,然后

从右往左找到小于key值得元素,最后再把key值和中间的值一交换,这时候,元素序列里,key左边的

是小于key的序列,右边的都是大于key的序列,这样用递归的思想,就可以不断的调用函数来实现快速排序

代码如下:

//////////////////////////////////////////////////
//交换法
//////////////////////////////////////////////////

void Swap2(int* a, int* b) {
	assert(a);
	assert(b);
	int tmp = *a;
	*a = *b;
	*b = tmp;
}

int Partion(int arr[], int _left, int _right) {
	assert(arr);
	int left = _left;
	int right = _right - 1;
	int key = arr[right];
	while (left < right) {
		/*
		**从左往右找到比key值大的元素
		*/
		while (left < right && arr[left] <= key) {
			++left;
		}
		/*
		**从右往左找到比key值小的元素
		*/
		while (left < right && arr[right] >= key) {
			--right;
		}
		/*
		**交换
		*/
		if (left < right) {
			Swap2(&arr[left], &arr[right]);
		}
	}
	/*
	**此时left指向的值一定大于key
	**如果是以为++left导致循环退出(如果没找到,那么肯定left >= right)
	**而right在上一次循环中已经是一个大于key的值了
	**如果是因为--right导致循环退出(和上面的情况一样)
	**left在上面的循环中已经找到了大于key的值
	*/
	//所以left指向的值一定大于key
	Swap2(&arr[left], &arr[_right - 1]);
	return left;
}

void QuickSort1(int arr[], int left, int right) {
	assert(arr);
	if (right - left <= 1) {
		return;
	}
	/*
	**具体思路如下:
	**每次都找出一个基准key值,然后把数组分为两半
	**左边是小于key的元素,右边是大于key的元素
	**再进行递归的处理,直到所有的元素都有序了
	*/
	int mid = Partion(arr, left, right);
	QuickSort1(arr, left, mid);
	QuickSort1(arr, mid + 1, right);
}

②挖坑法

挖坑法和交换法很类似,也是找到一个基准值key,然后定义left和right,从左往右找到大于key的值,然后把left

的值放到right的位置,再让right从右往左找到小于key的值,再把这个值放到left的位置,这样到最后,肯定会有

一个位置,把key放进去,这样就跟上一个方法一样了

代码如下:

///////////////////////////////////////
//挖坑法
///////////////////////////////////////

int Partion2(int arr[], int begin, int end) {
	assert(arr);
	int left = begin;
	int right = end - 1;
	int key = arr[right];
	while (left < right) {
		while (left < right && arr[left] <= key) {
			++left;
		}
		if (left < right) {
			arr[right--] = arr[left];
		}
		while (left < right && arr[right] >= key) {
			--right;
		}
		if (left < right) {
			arr[left++] = arr[right];
		}
	}
	arr[left] = key;
	return left;
}

void QuickSort2(int arr[], int left, int right) {
	assert(arr);
	if (right - left <= 1) {
		return;
	}
	int mid = Partion2(arr, left, right);
	QuickSort2(arr, left, mid);
	QuickSort2(arr, mid + 1, right);
}

③前后指针法

找到一个基准值,然后定义两个prev和next,next往前走,遇到小于key的值就停下来,然后再让prev往前走,如果

prev不等于next就交换他俩指向的值,然后最后判断,prev是否小于key,如果小于就一交换,这样,prev左边的值

就是小于它的元素,右边就是大于它的元素

代码如下:

////////////////////////////////////////////
//双指针前移法
////////////////////////////////////////////

int Partion3(int arr[], int begin, int end) {
	assert(arr);
	int prev = begin;
	int next = begin + 1;
	int key = begin;
	while (next <= end - 1) {
		if (arr[next] <= arr[key]) {
			++prev;
			if (prev != next) {
				Swap2(&arr[prev], &arr[next]);
			}
		}
		++next;
	}
	if (arr[prev] != arr[key]) {
		Swap2(&arr[prev], &arr[key]);
	}
	return prev;
}

void QuickSort3(int arr[], int left, int right) {
	assert(arr);
	if (right - left <= 1) {
		return;
	}
	/*前闭后开区间*/
	int mid = Partion3(arr, left, right);
	QuickSort3(arr, left, mid);
	QuickSort3(arr, mid + 1, right);
}

用非递归实现:

非递归实现用到了栈,代码如下:

/////////////////////////////////////////
//非递归版本
/////////////////////////////////////////
void QuickSortByLoop(int arr[], int size) {
	assert(arr);
	if (size <= 1) {
		return;
	}
	int left = 0;
	int right = size;
	SeqStack seq;
	SeqStackInit(&seq);
	SeqStackPush(&seq, right);
	SeqStackPush(&seq, left);
	/*如果当前栈不为空就一直循环*/
	while (!SeqStackEmpty(&seq)) {
		left = SeqStackTopValue(&seq);
		SeqStackPop(&seq);
		right = SeqStackTopValue(&seq);
		SeqStackPop(&seq);
		/*
		**判断条件
		*/
		if (right - left <= 1) {
			continue;
		}
		int mid = Partion3(arr, left, right);
		SeqStackPush(&seq, mid);
		SeqStackPush(&seq, left);
		SeqStackPush(&seq, right);
		SeqStackPush(&seq, mid + 1);
	}
}

栈的代码在末尾给出

三、选择类排序

每一趟在n – i + 1 ( i = 1,2,  , n - 1)个记录中选取关键字最小的记录作为有序序列中的第i个记录

1.选择排序

从第一个记录开始,通过n – 1次关键字的比较,从n个记录中选出关键字最小的记录,并和第一个记录进行交换。

第二趟从第二个记录开始,选择最小的和第二个记录交换。以此类推,直至全部排序完毕。

时间复杂度:T(n) = O(n²)

 空间复杂度:S(n) = O(1) 

 稳定性:不稳定排序

代码如下:

/*选择排序*/
void SelectSort(int arr[], int sz) {
	assert(arr);
	int i = 0;
	for (; i < sz--;) {
		/*每次选择出最大或者最小的元素,然后把这个元素放入数组的最后面*/
		int j = 1;
		int index = 0;
		for (; j < sz + 1; ++j) {
			if (arr[j] > arr[index]) {
				/*更新最大的元素下标*/
				index = j;
			}
		}
		/*交换最大值和当前数组最后一位*/
		Swap(&arr[index], &arr[sz]);
	}
}

2.堆排序

堆排序(Heapsort)是指利用堆积树(堆)这种 数据结构所设计的一种 排序算法,它是选择排序的一种。

可以利用 数组的特点快速定位指定索引的元素。堆分为大根堆和小根堆,是 完全二叉树

大根堆堆顶元素堆中,小根堆堆顶元素是最小的值,每次取出堆顶元素,然后调整堆,然后继续取元素,直到

堆为空,也得到了一个有序元素的序列

时间复杂度:T(n) = O(nn)

 空间复杂度:S(n) = O(1) 

  稳定性:不稳定排序

代码如下:

int Compare(int a, int b) {
	return a > b ? 1 : 0;
}

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

void AdjustDown(int arr[], int sz, int index) {
	assert(arr);

	/*
	**把堆中的堆顶个元素往下沉
	*/
	int parent = index;
	/*
	**左孩子节点
	*/
	int child = parent * 2 + 1;
	/*
	**看孩子节点是否小于sz
	*/
	while (child < sz) {
		/*
		**判断child是否越界
		**比较左孩子节点和右孩子节点,然后取其中的较小值
		*/
		if (child + 1 < sz && Compare(arr[child], arr[child + 1])) {
			child += 1;
		}
		/*
		**父节点和较小的子节点判断大小,如果大于较小的节点,就交换
		*/
		if (Compare(arr[parent], arr[child])) {
			/*交换*/
			Swap(&arr[parent], &arr[child]);
		}
		else {
			/*该数组已经是个堆了*/
			break;
		}
		/*更新父节点的子节点的值*/
		parent = child;
		child = parent * 2 + 1;
	}
}

/*堆排序*/
void HeapSort(int arr[], int sz) {
	assert(arr);

	//先把数组设置成堆
	int index = (sz - 1 - 1) / 2;
	while (index) {
		AdjustDown(arr, sz, index--);
	}
	AdjustDown(arr, sz, index);

	//然后再把堆首元素取出来放到堆的最后,对堆进行排序
	while (sz > 1) {
		/*
		**每次把堆顶元素和最后一个元素一交换
		**然后再对堆进行重新排序
		*/
		Swap(&arr[0], &arr[sz - 1]);
		--sz;
		AdjustDown(arr, sz, 0);
	}
}

涉及到堆的部分如果不懂可以在我关于堆的博客里看看

四、归并类排序

1.归并排序

假设初始序列右n个记录,首先将这n个记录看成n个有序的子序列,每个子序列的长度为1,然后两两归并,

得到n/2向上取整 个长度为2n为奇数时,最后一个序列的长度为1)的有序子序列。在此基础上,在对长

度为2的有序子序列进行两两归并,得到若干个长度为4的有序子序列。如此重复,直至得到一个长度为n

有序序列为止。

 时间复杂度:T(n) = O(nn)

 空间复杂度:S(n) = O(n)

 稳定性:稳定排序

代码如下:

把一个有序数组进行归并:

void _MergeArray(int arr[], int left, int mid, int right, int* tmp) {
	assert(arr);
	assert(tmp);
	int cur1 = left;
	int cur2 = mid;
	int tmp_index = left;

	while (cur1 < mid && cur2 < right) {
		/*
		**把给定范围的数组分为两半然后进行归并,得到一个有序的数组
		*/
		if (arr[cur1] < arr[cur2]) {
			tmp[tmp_index++] = arr[cur1++];
		}
		else {
			tmp[tmp_index++] = arr[cur2++];
		}
	}
	/*
	**把当前的接上
	*/
	while (cur1 < mid) {
		tmp[tmp_index++] = arr[cur1++];
	}
	while (cur2 < right){
		tmp[tmp_index++] = arr[cur2++];
	}
	memcpy(arr + left, tmp + left, sizeof(int)* (right - left));
}

递归版本归并排序:

/*
**前闭后开区间
*/
void _MergeSort(int arr[], int left, int right, int* tmp) {
	assert(arr);
	assert(tmp);
	if (right - left <= 1) {
		return;
	}
	int mid = left + (right - left) / 2;
	_MergeSort(arr, left, mid, tmp);
	_MergeSort(arr, mid, right, tmp);
	/*
	**先保证左右区间都是有序之后才能进行合并
	*/
	_MergeArray(arr, left, mid, right, tmp);
}

/*
**归并排序
*/
void MergeSort(int arr[], int size) {
	assert(arr);
	/*
	**申请的内从空间是用来暂时保存已经排好序的部分数组,最后重新复制给数组
	*/
	int* tmp = (int*)malloc(sizeof(arr)* size);
	_MergeSort(arr, 0, size, tmp);
	free(tmp);
}

非递归版本归并排序:

/*非递归版本的归并排序*/
void MergerSortByLoop(int* arr, int size) {
	assert(arr);
	if (size <= 1) {
		return;
	}
	int* tmp = (int*)malloc(sizeof(int)* size);
	int gap = 1;
	for (; gap < size; gap *= 2) {
		int i = 0;
		for (; i < size; i += 2 * gap) {
			int left = i;
			int mid = i + gap;
			int right = i + 2 * gap;
			if (mid > size) {
				mid = size;
			}
			if (right > size) {
				right = size;
			}
			_MergeArray(arr, left, mid, right, tmp);
		}
	}
	free(tmp);
}

上面用到的栈的代码:

SeqStack.h

#pragma once

#include<stdio.h>
#include<stddef.h>

typedef int SeqType;

#define SEQDATAMAX 1000

/*创建一个栈的结构体*/
typedef struct SeqStack{
	SeqType data[SEQDATAMAX];
	int top;
	int bottom;
}SeqStack;


/*初始化栈*/
void SeqStackInit(SeqStack* seq);
/*从栈里面压入一个元素*/
void SeqStackPush(SeqStack* seq, SeqType value);
/*出栈一个元素*/
void SeqStackPop(SeqStack* seq);
/*取栈顶元素*/
SeqType SeqStackTopValue(SeqStack* seq);
/*销毁栈*/
void SeqStackDestory(SeqStack* seq);
/*判断当前栈是不是空栈*/
int SeqStackEmpty(SeqStack* seq);

SeqStack.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"SeqStack.h"

/*初始化栈*/
void SeqStackInit(SeqStack* seq) {

	seq->top = 0;
	seq->bottom = seq->top;
}
/*从栈里面压入一个元素*/
void SeqStackPush(SeqStack* seq, SeqType value) {

	if (seq == NULL) {
		return;
	}
	/*判断栈是不是已经满了*/
	if (seq->top == SEQDATAMAX - 1) {
		return;
	}
	seq->top++;
	seq->data[seq->top] = value;
}
/*删除栈顶元素,出栈一个元素*/
void SeqStackPop(SeqStack* seq) {

	if (seq == NULL) {
		return;
	}
	/*判断栈是否为空栈*/
	if (seq->top == seq->bottom) {
		printf("栈为空");
		return;
	}
	seq->top--;
}
/*取栈顶元素*/
SeqType SeqStackTopValue(SeqStack* seq) {

	if (seq == NULL) {
		return;
	}
	return seq->data[seq->top];
}

/*销毁栈*/
void SeqStackDestory(SeqStack* seq) {

	if (seq == NULL) {
		return;
	}
	seq->top = 0;
}


/*判断当前栈是不是空栈*/
int SeqStackEmpty(SeqStack* seq) {
	if (seq == NULL) {
		return;
	}
	if (seq->top == seq->bottom) {
		return 1;
	}
	return 0;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值