数据结构---排序(插入排序、希尔排序、堆排序、选择排序、冒泡排序、快速排序、归并排序、计数排序)

一、插入排序

1.基本思想

       直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。 插入排序过程如下:

       

2.直接插入排序

算法思路

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

 

特性分析总结

时间复杂度:O(N^2)----最好情况:当元素全部有序时,时间复杂度最低,为O(N);当元素逆序时时间复杂度最大,为O(N^2)

空间复杂度:O(1)---只需要额外的空间tmp和end以及i三个整型变量

特点:元素集合越接近有序,时间效率越高(当元素全部有序时,时间效率最高为O(N));直接插入排序算法是一种稳定的排序算法。

稳定性:稳定,当两个元素相等时不后移,该算法就是稳定的算法(稳定性值得是排序前后元素的相对位置不变)

代码描述

// 插入排序
void InsertSort(int* a, int n)
{
	assert(a);
	//算法思路:将数组中下标为i的元素插入到数组中[0,i-1]的有序区间中,使数组完全有序
	for (int i = 0; i < n-1; i++)
	{
		//将下标为i+1的元素跟[0,i]的元素进行比较,如果大于就将该元素后移,遇到小于的停止并将tmp插入
		int end = i;
		int tmp = a[end + 1];
		while (end >= 0)
		{
			if (a[end] > tmp)
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = tmp;
	}
}

3.希尔排序

       希尔排序法又称缩小增量法。希尔排序法的基本思想是:以gap为间隔进行插入排序,gap逐次减小,最后一趟gap为0(直接插入排序)。

 
特性总结
 
1) 希尔排序是对直接插入排序的优化。
 
2)  gap > 1 时都是预排序,目的是让数组更接近于有序。当 gap == 1 时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
 
3) 希尔排序的时间复杂度不好计算,需要进行推导,推导出来平均时间复杂度: O(N^1.3—N^2
 
4) 稳定性:不稳定(间隔不同,每次的相对位置都有可能发生改变)
 
5)时间复杂度:O(N^1.3)~O(N^2)
 
6)空间复杂度:O(1)
 
代码描述
 
// 希尔排序
void ShellSort(int* a, int n)
{
	//算法思路:以gap为间隔进行插入排序,gap逐次减小,最后一趟gap为0(直接插入排序)
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 3 + 1;//gap以三倍进行减小,为了保证最后一次是1所以要加1
		for (int i = 0; i < n - gap - 1; i++)
		{
			//以gap为间隔进行插入排序
			int end = i;
			int tmp = a[end + 1];
			while (end >= 0)
			{
				if (a[end]>tmp)
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			//将tmp插入到正确位置
			a[end + gap] = tmp;
		}
	}
}

二、选择排序

1.基本思想

       每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。

2.直接选择排序

基本步骤

在元素集合array[i]--array[n-1]中选择关键码最大()的数据元素

若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换

在剩余的array[i]--array[n-2]array[i+1]--array[n-1])集合中,重复上述步骤,直到集合剩余1个元素

特性总结

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

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

空间复杂度:O(1)

稳定性:不稳定(将数组后边的元素与最大数进行交换时,很有可能将数组元素的相对位置改变)

代码描述

// 选择排序
void SelectSort(int* a, int n)
{
	//迭代n-1次,每次选择最小的数与i
	for (int i = 0; i < n-1; i++)
	{
		int minIndex = i;
		//从[i,n-1]中找出最小的一个数,如果这个数下标不是i则和i进行交换
		for (int j = i + 1; j < n; j++)
		{
			if (a[j] < a[minIndex])
			{
				minIndex = j;
			}
		}
		swap(&a[i],&minIndex);
	}
}


//扩展:选择排序的优化,每次选出一个最大一个最小的数,最小的数放在起始位置最大的数放在结束位置

// 选择排序的优化
void SelectSort2(int* a, int n)
{
	//迭代n/2次,每次选择最小的数和最大的数
	for (int i = 0; i < n/2; i++)
	{
		int minIndex = i;
		int maxIndex = i;
		//从[i,n-i]中找出最小的一个数和一个最大的数,将最小的数放在数组起始位置最大的数放在数组结束位置
		for (int j = i + 1; j < n-i; j++)
		{
			if (a[j] < a[minIndex])
			{
				minIndex = j;
			}
			else if (a[j] > a[maxIndex])
			{
				maxIndex = j;
			}
		}
		swap(&a[i], &a[minIndex]);
		//当最小的数在i位置,最大的数在最后位置时,一次交换两个数都在合适的位置了,不需要再对大数进行交换
		if (maxIndex != i)
			swap(&a[n-i-1],&a[maxIndex]);
	}
}

3.堆排序

       堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。

特性总结

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

时间复杂度:O(N*logN)---n个节点的二叉树的深度为logN,每进行一次排序就要对二叉树进行一次调整,一次调整的时间复杂度为logN,共N次调整的时间复杂度为N*longN

空间复杂度:O(1)

稳定性:不稳定(利用向下调整算法进行调整的时候相对位置可能改变)

代码描述

// 堆排序
void AdjustDwon(int* a, int n, int root)
{
	//排升序需要建大堆,因此向下调整算法应该选择左右孩子中较大的一个跟父节点比较交换
	int parent = root;
	//父节点和孩子节点的关系
	int child = parent * 2+1;
	while (child < n)
	{
		//保证child为左右孩子中较大的一个(右孩子要存在)
		if (child+1 < n && a[child + 1] > a[child])
			child++;

		//如果孩子节点大于父节点,则交换
		if (a[child] > a[parent])
			swap(&a[child], &a[parent]);
        else
            break;
		parent = child;
		child = parent * 2+1;
	}
}
void HeapSort(int* a, int n)
{
	//建大堆,从最后一个父节点开始,向前依次使用向下调整算法
	for (int i = (n - 2) / 2; i >= 0; i--)
	{
		AdjustDwon(a,n,i);
	}
	//堆排序,每次将堆顶元素与数组最后一个元素进行交换,在对堆进行向下调整
	for (int i = 0; i < n - 1; i++)
	{
		swap(&a[0], &a[n - i - 1]);
		AdjustDwon(a,n-i-1,0);
	}
}

三、交换排序

1.基本思想

       所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。

2.冒泡排序

特性总结

冒泡排序是一种非常容易理解的排序

时间复杂度:O(N^2);最好情况:当数据有序时时间复杂度最低(利用exchage记录如果一趟排序没有任何的数据交换,则证明有序直接退出,时间复杂度为O(N)),为O(N);

空间复杂度:O(1)

稳定性:稳定

代码描述

// 冒泡排序
void BubbleSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		int exchage = 0;
		//两两进行比较,将大数往后冒,一趟排将最大的数放在最后边
		for (int j = 0; j < n - i - 1; j++)
		{
			if (a[j] > a[j + 1])
			{
				swap(&a[j], &a[j + 1]);
				//如果第一趟排序没有任何数据交换,则证明已经有序不需要再进行排序
				exchage = 1;
			}
		}
		if (exchage == 0)
			break;
	}
}

3.快速排序

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

1.整体思想

2.代码描述

1)快速排序递归实现(3种前后指针、左右指针、挖坑法)

三数取中(求中间数,将有序情况下的最坏变成最优使时间复杂度降低)

int GetMidIndex(int* a, int left, int right)
{
	int mid = (left+right) / 2;
	if (a[left] > a[mid])
	{
		if (a[right] > a[left])
			return left;
		else if (a[right] < a[mid])
			return mid;
		else
			return right;
	}
	else
	{
		if (a[right] > a[mid])
			return mid;
		else if (a[right] < a[left])
			return left;
		else
			return right;
	}
}

左右指针(单趟排序)

// 快速排序hoare版本(左右指针法)
int PartSort1(int* a, int left, int right)
{
	//当数组中的数完全有序时,时间复杂度为O(N^2),为了解决这个问题引入三数取中,让该情况下时间复杂度为O(N*logN)
	//三数取中是从最左边、最右边和中间这三个数中取出值处于中间位置的数与最右边的数进行交换。这样就将最坏的情况变成了最好的情况
	int midIndex = GetMidIndex(a,left,right);
	swap(&a[right],&a[midIndex]);

	int keyIndex = right;
	while (left < right)
	{
		//左边找到比key值大的数停下来
		while (left < right && a[left] <= a[keyIndex])
		{
			left++;
		}
		//右边找比key小的值停下来
		while (left < right && a[right] >= a[keyIndex])
		{
			right--;
		}
		//左边找到的大的和右边找到的小的交换
		swap(&a[left],&a[right]);
	}
	//当left==right时,1.left右走走到right位置:right位置的值比keyindex大
	//2.right左走走到left位置时,因为left 比right早走,所以此时left已经找到比keyindex值大
	//此时left==right且大于keyindex,交换keyindex和left的值,完成一趟排序
	swap(&a[left],&a[keyIndex]);

	return left;
}

挖坑法(单趟排序)

// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
	//先在最右边挖一个坑
	int key = a[right];

	while (left < right)
	{
		//左边找一个比key大的数填到右边的坑,左边形成一个新的坑
		while (left < right && a[left] <= key)
		{
			left++;
		}
		a[right] = a[left];
		//右边找一个比key小的数填到左边的坑,右边又形成一个新的坑
		while (left < right && a[right] >= key)
		{
			right--;
		}
		a[left] = a[right];
	}
	//让key填入最后的坑中,此时数组中可以左边的数都比他小右边的数都比它大
	a[left] = key;

	return left;
}

前后指针法(单趟排序)

// 快速排序前后指针法
int PartSort3(int* a, int begin, int end)
{
	int midIndex = GetMidIndex(a, begin, end);
	swap(&a[begin], &a[end]);
	//curr找小,跟prev交换,大的curr往后走,prev 不动
	int cur = begin;
	int prev = cur-1;
	while (cur <= end)
	{
		if (a[cur] <= a[end] && ++prev != cur)
			swap(&a[cur],&a[prev]);

		cur++;
	}
	return prev;
}

使用上述单趟排序进行递归

void QuickSort(int* a, int left, int right)
{
	//左右指针递归法
	/*if (left >= right)
		return;
	int div = PartSort1(a,left,right);
	QuickSort(a,left,div-1);
	QuickSort(a,div+1,right);*/

	//前后指针法
	if (left >= right)
		return;
	int div = PartSort3(a, left, right);
	QuickSort(a, left, div - 1);
	QuickSort(a, div + 1, right);
}

2)快速排序非递归实现

思路:建立一个栈,让begin和end入栈,进行排序时在读出bengin和end进行排序,完成一趟排序。一趟排序结束后,在让左边和右边继续入栈进行排序。

代码描述

// 快速排序 非递归实现
void QuickSortNonR(int* a, int left, int right)
{
	//定义一个栈并初始化
	Stack st;
	StackInit(&st);

	//让left和right入栈
	StackPush(&st,right);
	StackPush(&st,left);
	//当栈不为空时,就一直进行快速排序
	while (!StackEmpty(&st))
	{
		//让栈中的left和right之间的数进行一趟快速排序
		int begin = StackTop(&st);
		StackPop(&st);
		int end = StackTop(&st);
		StackPop(&st);
		int div = PartSort1(a,begin,end);
		//判断div的左右两侧是否还需要继续进行快速排序
		if (div + 1 < end)
		{
			StackPush(&st,end);
			StackPush(&st,div+1);
		}
		if (div - 1 > begin)
		{
			StackPush(&st,div-1);
			StackPush(&st,begin);
		}
	}

	//销毁栈
	StackDstory(&st);
}

四、归并排序

1.基本思想:

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

2.递归实现归并排序

//合并数组
void MergeArr(int* a,int begin1,int begin2,int end1,int end2,int* tmp)
{
	int left = begin1;
	int right = end2;
	int index = begin1;
	//先遍历两个数组,每次选较小的数放入tmp中,当其中一个数组遍历完停止
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
			tmp[index++] = a[begin1++];
		else
			tmp[index++] = a[begin2++];
	}
	//将另一个没有被遍历完的数组直接拷贝到数组中
	while (begin1 <= end1)
		tmp[index++] = a[begin1++];
	while (begin2 <= end2)
		tmp[index++] = a[begin2++];
	//把归并好(排完序)数组在拷贝到原数组中
	for (int i = left; i <= right; i++)
		a[i] = tmp[i];

}
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);

	//合并数组
	MergeArr(a,left,mid+1,mid,right,tmp);
}
void MergeSort(int* a, int n)
{
	//开一个额外的数组空间
	int* tmp = (int*)malloc(sizeof(int)*n);

	//将数组进行拆解
	_MergeSort(a,0,n-1,tmp);

	free(tmp);
}

3.非递归实现归并排序

算法思路:逆向思维,取消归并排序的分解过程,从单独数据开始直接利用循环进行合并

// 归并排序非递归实现
void MergeSortNonR(int* a, int n)
{
	//思路:逆向思维,取消归并排序的分解过程,从单独数据开始直接利用循环进行合并
	int* tmp = (int*)malloc(sizeof(int)*n);
	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i += 2 * gap)
		{
			//如果只有左部分没有又部分就不在合并
			if (i + gap >= n)
				break;
			//右部分的数个数不够,调整右部分的右边界
			if (i+2*gap-1 >= n)
				MergeArr(a, i, i + gap, i + gap - 1, n-1, tmp);
			else
				MergeArr(a, i, i + gap, i + gap - 1, i + 2 * gap - 1, tmp);
		}
		gap *= 2;
	}
}

4.特性总结

1) 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。

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

3)空间复杂度:O(N)

4) 稳定性:稳定

五、计数排序

算法思想

找到数组中的最大数max和最小数min,额外开辟一个max-min的数组空间,其中数组下标表示数值,遍历原数组,将数组中的值所对应的下标中的值记录要排序的数组的元素的出现此处。在对数组进行打印。

代码描述

// 计数排序
void CountSort(int* a, int n)
{
	//找到最大数和最小数
	int max = a[0];
	int min = a[0];
	for (int i = 0; i < n; i++)
	{
		if (a[i] > max)
			max = a[i];
		else if (a[i] < min)
		min = a[i];
	}
	//开辟数组max-min+1
	int* tmp = (int*)malloc(sizeof(int)*(max-min+1));
	memset(tmp,0,sizeof(int)*(max-min+1));
	//统计个数
	for (int i = 0; i < n; i++)
	{
		tmp[a[i] - min]++;
	}
	//排序拷贝
	int index = 0;
	for (int i = 0; i < max - min + 1; i++)
	{
		while (tmp[i])
		{
			a[index++] = i + min;
			tmp[i]--;
		}
	}
}

特性总结

1) 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。

2) 时间复杂度:O(MAX(N,范围))

3) 空间复杂度:O(范围)

4)  稳定性:稳定

猜你喜欢

转载自blog.csdn.net/qq_47406941/article/details/113075781