【数据结构】八大排序算法(以升序为例)

目录

​编辑 

一、直接插入排序

基本思想

实现

特点

二、希尔排序

基本思想

实现

 特点

三、选择排序

基本思想 

实现

特点

四、堆排序

基本思想

实现

特点 

五、冒泡排序

基本思想

实现

特点 

六、快速排序

1.hoare版本

基本思想

实现

2.挖坑法

基本思想

实现

3.前后指针法

基本思想

实现

4.非递归版本

基本思想

实现

5.总结

七、归并排序

基本思想

实现

特点

八、计数排序

 基本思想 

实现

特点


一、直接插入排序

 

基本思想

1.在前n个有序元素中插入一个数x。

2.从第n个数开始依次和x进行比较,比x大的数依次向后移一个单位,遇到比x小的元素,停止比较,插入x。

3.从数组的第一个元素开始,依次执行1和2直到整个数组有序。

实现

对于一个数组长度为n的整型数组,先考虑单趟的情况。

对于[0,end]的区间,将a[end+1]插入到合适的位置,区间内的元素为升序。

1.如果a[end+1]比至少一个区间内的元素大,则将其插入位置为比它小和比它大的元素中间;

2.如果a[end+1]比区间中的所有元素都小,则其插入位置为数组的首位。

代码:

void InsertSort(int* arr, int n)
{
	int end = 0;
	for (int i = 0;i < n - 1;i++)
	{
		end = i;
		int x = arr[end + 1];//即将要插入前[0,end]区间元素的值
		while (end >= 0)
		{
            //比x大的元素都往后挪一位
			if (arr[end] > x)
			{
				arr[end + 1] = arr[end];
				end--;
			}
			else
			{
				break;
			}
		}
		arr[end + 1] = x;//插入
	}
}

特点

1.数组元素越有序,直接插入排序的效率越高。

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

当数组为逆序排列时,为最坏的情况,时间复杂度的精确值为1+2+3+...+(n-1)=n(n-1)/2

3.空间复杂度O(1)

二、希尔排序

基本思想

1.希尔排序于直接插入排序的区别是引入了间隔gap,对于一个数组而言,先进行预排序,再进行直接插入排序。

2.gap不断缩小的过程也就是预排序的过程。

3.gap=1则为直接插入排序。

实现

1.间隔gap大于1时的预排序本质也是插入排序。

代码:

void ShellSort(int* arr, int n)
{
	int gap = n;
	while(gap > 1)
	{
        //当gap>1时为预排序;gap=1时为直接插入排序
        //预排序后数组已经接近有序,直接插入排序的效率高
		gap = gap / 2;
		for (int j = 0;j < n - gap;j++)
		{
			int end = j;
			int x = arr[end + gap];
			while (end >= 0)
			{
				if (arr[end] > x)
				{
					arr[end + gap] = arr[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			arr[end + gap] = x;
		}
	}
}

 特点

1.希尔排序是直接插入排序的变式

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

3.空间复杂度O(1)

三、选择排序

基本思想 

1.从数组中找到最大值或最小值;

2.将最大值或最小值与数组最右边或最左边的值进行交换。

3.对剩余数组元素(已经交换至最右边或最左边的元素不再参与)继续进行操作1和2。

实现

除了最基础的选择排序,还可进行优化,单趟排序查找数组的最大和最小元素,将其分别放到数组的最右端和最左端,然后对剩余数组做相同的操作,直到数组有序。

双指针法(双下标)。

代码:

void SelectSort(int* arr, int n)
{
	int begin = 0;
	int end = n - 1;
    //保证剩余数组部分不为空
	while (begin < end)
	{
        //设置数组最大值和最小值的初始下标为begin
		int min = begin;
		int max = begin;
        //查找数组元素的最大值和最小值,并记录下标max和min
		for (int i = begin + 1;i <= end;i++)
		{
			if (arr[i] < arr[min])
			{
				min = i;
			}
			if (arr[i] > arr[max])
			{
				max = i;
			}
		}
        //将数组元素的最小值和数组首元素交换
		Swap(&arr[min], &arr[begin]);
        //由于数组首元素已经置换成最小元素,如果本来是数组最大元素,则需进行特殊处理,将最 
          大元素的下标置为最小元素的原下标
		if (begin == max)
		{
			max = min;
		}
        //将数组元素的最大值和数组最后一个元素交换
		Swap(&arr[max], &arr[end]);
        //交换后原数组的最大元和最小元素不参与后续的排序
		begin++;
		end--;
	}
}

特点

1.选择排序的工作量与数组长度直接挂钩,与数组的有序程度无关。

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

3.空间复杂度O(1),没有开辟额外的空间。

四、堆排序

基本思想

1.排升序,建大堆;排降序,建小堆。

(建大堆后,交换堆顶和最后一个元素,交换后不考虑最后一个元素,因为堆是用数组进行存储的,堆顶元素即数组的第一个元素,最后一个元素即数组的最后一个元素,所以排升序是建大堆,反之亦然。)

2.建堆结束后,开始进行排序的中心步骤——堆顶元素和最后一个元素进行交换,交换后的最后一个元素排除在外,堆顶元素进行向下调整。

3.继续执行步骤2,直到所有元素都被排除在外。

实现

1.建大堆可通过从最右下角的子树开始,从右向左,总下到上执行向下调整算法。

2.整个算法都是在原数组上进行操作,无需开辟额外空间。

代码:

//向下调整 建大堆
void AdjustDown(int* arr, int n, int parent)
{
	assert(arr);
	int child = parent * 2 + 1;
	while (child < n)
	{
		if (child + 1 < n)
		{
			child = arr[child] > arr[child + 1] ? child : child + 1;
		}
		if (arr[parent] < arr[child])
		{
			Swap(&arr[parent], &arr[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

//堆排序
void HeapSort(int* arr, int n)
{
	//建大堆
	for (int i = (n - 1 - 1) / 2;i >= 0;i--)
	{
		AdjustDown(arr, n, i);
	}

	//交换堆顶和堆中最后一个元素
	//将end-1
	//向下调整
	int end = n - 1;
	while (end > 0)
	{
		Swap(&arr[0], &arr[end]);
		AdjustDown(arr, end, 0);
		end--;
	}
}

特点 

1.数组元素的有序性对堆排序几乎没什么影响,建大堆本身会使数组变得无序。

2.时间复杂度O(NlogN)

3.空间复杂度O(1),没有开辟额外的空间。

五、冒泡排序

基本思想

1.从数组首元素开始,依次和右边的元素进行大小比较,如果比右边的元素大就进行值交换。

2.单趟排序至少能让最大的元素归位,最大的元素归位后不再参与后续的排序。

3.重复执行步骤1,最糟糕的情况需要有n-1趟排序。

4.设置变量来标志当趟排序是否有值交换,若没有则说明当趟排序时数组已经有序,无需再继续排序。

实现

1.利用循环实现多趟排序,设置flag来标志数组是否有序。

代码:

void BubbleSort(int* arr, int n)
{
	for (int i = 0;i < n;i++)
	{
		int flag = 0;//假设数组已经有序
		for (int j = 0;j < n - i - 1;j++)
		{
            //比较元素和右边相邻元素的大小,比右边元素大则进行值交换
			if (arr[j] > arr[j + 1])
			{
				Swap(&arr[j], &arr[j + 1]);
				flag = 1;
			}
		}
		//已经有序了,无需继续进行排序
		if (flag == 0)
			break;
	}
}

特点 

1.由于设置了标志变量,数组越接近有序,排序的次数会越少。

2.但当数组元素较大时优化效果微乎其微。

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

4.空间复杂度O(1),没有开辟额外的空间。

六、快速排序

1.hoare版本

基本思想

1.将数组最左边元素的值赋值给key。

2.设置左右指针L和R,L和R错开移动,最左边元素作key,则R先动,以保证L和R相遇时所指的元素值小于key。

3.R指向的元素大于key时R向左移动,遇到小于key的元素时停止,然后L移动,遇到大于key的元素时停止,交换L和R所指元素。

4.再次执行步骤3,直到L和R相遇。

5.相遇后,交换相遇处的元素和最左边的元素。

6.递归实现整个数组的排序。

实现

1.用双指针,错开移动,相遇结束。

2.单趟排序结束后key归位,然后再分别对key左右两边的数组部分进行相同的操作,可用递归实现。

代码:

// 快速排序hoare版本
int PartSort1(int* arr, int begin, int end)
{
	if (begin >= end)
		return -1;

	int left = begin, right = end;
	int key_i = left;//左端点的值做key
	while (left < right)
	{
		//right先走,找小
		while (left < right && arr[right] >= arr[key_i])
		{
			--right;
		}
		//left再走,找大
		while (left < right && arr[left <= arr[key_i]])
		{
			++left;
		}
		Swap(&arr[left], &arr[right]);
	}
	Swap(&arr[left], &arr[key_i]);
	key_i = left;

	return key_i;
}

void QuickSort(int* arr, int begin, int end)
{
	if (begin >= end)
		return;
	int key_i = PartSort1(arr, begin, end);
	//递归
	QuickSort(arr, begin, key_i - 1);
	QuickSort(arr, key_i + 1, end);

}

2.挖坑法

基本思想

1.挖坑版是hoare的“民间版“,挖坑的方法便于理解。

2.将数组首元素赋值给key,将首元素位置视为空,设置左右指针L和R,左右指针错开填坑。

3.右指针先动,遇到比key小的元素是停下来,将该较小元素填入首元素的位置,得到一个新的坑位;左指针开始移动,遇到比key大的元素停下来,将该较大元素填入新的坑位,直到L和R相遇,将key填入相遇时的坑位。

4.一轮操作后,key找准了自己的位置,而后对key左右的部分数组进行相同的操作,利用递归来实现。

实现

与hoare版本相似

代码:

// 快速排序挖坑法
int PartSort2(int* arr, int begin, int end)
{
	if (begin >= end)
		return -1;

	int left = begin, right = end;
	int key = arr[left];
	int hole = left;//坑在左端
	while (left < right)
	{
		//right先走,找小
		while (left < right && arr[right] >= key)
		{
			--right;
		}
		//填左边的坑
		arr[hole] = arr[right];
		hole = right;//坑在右端

		//left再走,找大
		while (left < right && arr[left] <= key)
		{
			++left;
		}
		//填右边的坑
		arr[hole] = arr[left];
		hole = left;
	}
	arr[hole] = key;
	return hole;
}

void QuickSort(int* arr, int begin, int end)
{
	if (begin >= end)
		return;
	int key_i = PartSort2(arr, begin, end);
	//递归
	QuickSort(arr, begin, key_i - 1);
	QuickSort(arr, key_i + 1, end);

}

3.前后指针法

 

基本思想

1.数组的首元素赋值给key。

2.设置前后指针prev和cur,将指针分别初始化为第一个元素和第二个元素的下标。

3.以cur为下标的元素与key比较,比key小则prev后移一位后与cur所指元素进行交换,否则cur向前挪动,prev不动。

4.执行3操作直到cur遍历整个数组,将prev所指元素和首元素进行交换,交换后key归位。

5.对key左右两边的数组进行相同的操作,利用递归实现。

实现

代码:

// 快速排序前后指针法
int PartSort3(int* arr, int begin, int end)
{
	if (begin >= end)
		return -1;
	int prev = begin, cur = begin + 1;
	int key_i = begin;
	while (cur <= end)
	{
		//cur找比key小的值,找到后prev++,然后交换arr[prev]和arr[cur]
		if(arr[cur] < arr[key_i])
		{
			Swap(&arr[++prev], &arr[cur]);
		}
		cur++;
	}
	Swap(&arr[prev], &arr[key_i]);
	key_i = prev;
	return key_i;
}

void QuickSort(int* arr, int begin, int end)
{
	if (begin >= end)
		return;
	int key_i = PartSort3(arr, begin, end);
	//递归
	QuickSort(arr, begin, key_i - 1);
	QuickSort(arr, key_i + 1, end);

}

4.非递归版本

基本思想

1.利用数据结构——栈,存储进行排序的起始和终止下标;

2.利用获取栈顶元素和出栈的方法以及栈“先进后出”的特点将key的左右区间的起止下标存入栈(先右后左);

3.获取栈顶元素、出栈相结合,得到待排序数组的起始和终止下标,调用三种排序函数中的一个,进行排序。

4.2和3相结合,直到栈为空,排序结束。

实现

代码:

// 快速排序 非递归实现
void QuickSortNonR(int* arr, int begin, int end)
{
	Stack st;
	StackInit(&st);
	StackPush(&st, begin);
	StackPush(&st, end);

	while (!StackEmpty(&st))
	{
		int right = StackTop(&st);
		StackPop(&st);
		int left = StackTop(&st);
		StackPop(&st);

		int key_i = PartSort3(arr, left, right);
		if (right > key_i+1)
		{
			StackPush(&st, key_i + 1);
			StackPush(&st, right);
		}
		if (left < key_i-1)
		{
			StackPush(&st, left);
			StackPush(&st, key_i - 1);
		}	
	}
	StackDestroy(&st);
}

5.总结

1.快速排序效率高,但递归调用栈帧消耗大,当数组长度较大时,可用通过小区间采用插入排序,以减少递归调用次数来减少调用栈帧的消耗。

2.当被排序数组几乎接近有序时,快排处于劣势,时间复杂度为O(N^2),可采用三数取中的方法来进行优化。但是对于几乎全部相同的数组这种方法不能解决。

代码:

//三数取中
int GetMidIndex(int* arr, int begin, int end)
{
	int mid = (begin + end) / 2;
	if (arr[begin] < arr[end])
	{
		if (arr[begin] > arr[mid])
			return begin;
		else
		{
			if (arr[mid] < arr[end])
				return mid;
			else
				return end;
		}
	}
	else//arr[begin] > arr[end]
	{
		if (arr[end] > arr[mid])
			return end;
		else
		{
			if (arr[begin] > arr[mid])
				return mid;
			else
				return begin;
		}
	}
}

3.时间复杂度O(NlogN)

4.空间复杂度O(logN)    递归建立函数栈帧

七、归并排序

 

基本思想

1.归并排序的基本思想时分治思想,即分而治之。

2.将数组进行区间的划分,将一个个区间进行排序,而单个元素可以被视为一个个有序的区间,所以”分“的最底层结果是将数组拆分成一个个元素。

3.将有序的区间进行合并,再重新排序使之有序。排序的方法是在一个额外的数组空间里进行”尾插“,即将两个有序区间的较小值进行插入,直到两个区间的值都插入完毕,得到的即为两个有序区间合并后的有序区间。

4.用递归的思想或非递归的思想来实现区间的”分“和”并“。

实现

1.递归版本

与二叉树的前序遍历相似。

代码:

void _MergeSort(int* a, int begin, int end, int* tmp)
{
	//理论上不可能出现begin>end的情况,但是当begin=end时,
	//默认该区间为有序,不用进行归并,这是递归的出口
	if (begin >= end)
		return;

	int mid = (begin + end) / 2;
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;
	//递归
	_MergeSort(a, begin1, end1, tmp);
	_MergeSort(a, begin2, end2, tmp);

	//归并
	int i = begin;
	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 + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}

// 归并排序递归实现
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc failed!");
	}

	_MergeSort(a, 0, n - 1, tmp);
	//释放动态申请的内存
	free(tmp);
	tmp = NULL;
}

2.非递归版本

递归版本调用堆栈较多,为了减少额外空间的消耗,可以利用循环的方法来控制区间大小,为非递归方法实现归并排序的关键。与递归不同的是,非递归的单趟循环会使整个数组的相同小区间都变得有序,区间大小的两倍大于等于数组后,整个数组都有序。

代码:

// 归并排序非递归实现
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	// 归并每组数据个数,从1开始,因为1个认为是有序的,可以直接归并
	int rangeN = 1;
	while (rangeN < n)
	{
		for (int i = 0; i < n; i += 2 * rangeN)
		{
			// [begin1,end1][begin2,end2] 归并
			int begin1 = i, end1 = i + rangeN - 1;
			int begin2 = i + rangeN, end2 = i + 2 * rangeN - 1;
            //控制临时数组的下标
			int j = i;

			// end1 begin2 end2 越界
			// 修正区间  ->拷贝数据 归并完了整体拷贝 or 归并每组拷贝
			if (end1 >= n)
			{
				end1 = n - 1;
				// 不存在区间
				begin2 = n;
				end2 = n - 1;
			}
			else if (begin2 >= n)
			{
				// 不存在区间
				begin2 = n;
				end2 = n - 1;
			}
			else if (end2 >= n)
			{
				end2 = n - 1;
			}

			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] <= a[begin2])
				{
					tmp[j++] = a[begin1++];
				}
				else
				{
					tmp[j++] = a[begin2++];
				}
			}

			while (begin1 <= end1)
			{
				tmp[j++] = a[begin1++];
			}

			while (begin2 <= end2)
			{
				tmp[j++] = a[begin2++];
			}
		}

		// 也可以整体归并完了再拷贝
		memcpy(a, tmp, sizeof(int) * n);

		rangeN *= 2;
	}

	free(tmp);
	tmp = NULL;
}

特点

1.需要借助额外的数组,空间复杂度为O(N)

2.时间复杂度O(NlogN),数组的有序性不影响算法的效率。

3.分而治之思想的应用。

八、计数排序

 基本思想 

1.找到数组a的最大元素和最小元素,计算range = a[max]-a[min]+1,动态申请额外的数组空间tmp,长度为range,并初始化数组元素为0;

2.对以a[i]-min为下标的tmp元素+1,从而达到计数的效果。

3.遍历整个数组a,执行操作2,然后再将数组tmp中的元素下标+min拷贝回原数组,拷贝一个元素-1,值为0时拷贝下一个,直到tmp数组全为0,得到的拷贝数组即为排序后的数组。

实现

代码:

// 计数排序
void CountSort(int* a, int n)
{
	int min = a[0], 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* tmp = (int*)calloc(range, sizeof(int));
	if (tmp == NULL)
	{
		perror("calloc fail");
		exit(-1);
	}
	//1.计数
	for (int i = 0;i < n;i++)
	{
		tmp[a[i] - min]++;
	}
	//2.排序
	int i = 0;
	for (int j = 0;j < range;j++)
	{
		while(tmp[j] > 0)
		{
			a[i++] = j + min;
			tmp[j]--;
		}
	}
	free(tmp);
	tmp = NULL;
}

特点

1.非比较排序,不是通过数组元素之间进行比较来进行排序;

2.有局限性,适用于极差小的数组。数组元素越集中,算法的效率越高。

3.只适合整型,不能用于浮点数的排序。

4.时间复杂度O(N+range)

5.空间复杂度O(range)

九、排序算法的稳定性

排序算法的稳定性是指,对于未排序前的数组,排序后相同值的元素的相对位置有没有可能会发生改变;排序后,相同元素的相对位置不发生改变,则该排序算法是稳定的,反之亦然。

故,上述八大排序中,稳定排序有:直接插入排序、冒泡排序、归并排序、计数排序;

不稳定排序有:希尔排序、选择排序、堆排序、快速排序。

(只有在排序前相同元素的顺序有意义时,稳定性才有意义。)

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
数据结构中的八大排序算法,是指常见的八种用于对数据进行排序算法。这八种算法分别是冒泡排序、选择排序、插入排序、希尔排序、归并排序、快速排序、堆排序、计数排序和基数排序。 冒泡排序是一种简单的排序算法,通过不断比较和交换相邻元素的位置,使得最大(或最小)的元素逐渐往后(或往前)移动。 选择排序是一种简单直观的排序算法,每次选择未排序序列中最小(或最大)的元素,放到已排序序列的末尾。 插入排序是一种简单直观的排序算法,将一个待排序的元素插入到已部分排序的数列中的合适位置。 希尔排序是一种改进的插入排序算法,通过将待排序数列分组,并对每个分组进行插入排序,然后逐渐减小分组规模,最后进行一次插入排序。 归并排序是一种分治思想的排序算法,将待排序数列不断分割成较小的数列,然后再将这些较小的数列按照顺序进行合并。 快速排序是一种分治思想的排序算法,通过选择一个中间的基准元素,将数列分割成两部分,然后分别对这两部分进行排序。 堆排序是一种利用堆这种数据结构排序算法,通过将待排序数列构建成一个大(或小)顶堆,然后逐步将堆顶元素与最后一个元素交换,并调整堆结构。 计数排序是一种非比较型的排序算法,通过统计待排序数列中每个元素出现的次数,然后依次输出即可。 基数排序是一种非比较型的排序算法,通过对待排序数列的每个位进行排序,依次从低位到高位进行。 这里简单介绍了八大排序算法的基本思想和实现方法。在实际应用中,不同的排序算法适用于不同的场景和要求,我们需要根据具体情况选择合适的算法

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值