常见排序代码讲解

一、插入排序

思想:

对于未排好序的数据,在已经排好序的序列中依次往后查找,找到相应位置并插入

日常生活中,我们打扑克所用的整理牌的方法就为 插入排序

当插入第i个元素的时候,前面的arr[i-1],ar[i-2]......arr[0]已经全部排序好,只需要用当前位置的元素arr[i]与arr[i-1],arr[i-2]......依次比较,找到插入位置将其插入即可

// 插入排序
void InsertSort(int* arr, int n)
{
	//从第一个元素开始比较
	for (int i = 1; i < n; ++i)
	{
		//让end保存当前元素的前一个位置
		int end = i - 1;
		//key保存当前元素
		int key = arr[i];
		//如果end>=0&&当前元素key小于前一个元素,前一个元素向后搬移,再使end--
		while (end>=0 &&key<arr[end])
		{
			arr[end + 1] = arr[end];
			end--;
		}
		//循环退出说明,此位置之前没有元素或者,此位置之前的元素都比key小
		//插入key
		arr[end + 1] = key;
	}
}

时间复杂度:

最好情况 O(n)        最坏情况O(n^2)         

空间复杂度:O(1)

稳定性:稳定

二、希尔排序

思想:

先选定一个整数grp,然后根据这个数对数组分组,依次对分组的每一组数据进行插入排序,然后重复上述步骤,直到grp==1时结束(当grp==1时,相当于整个序列被分为1组,此时,只需进行一次插入排序即可)

// 希尔排序
void ShellSort(int* arr, int n)
{
	//先选定一个整数
	int grp = 5;
	while (grp >=1)
	{
		//将数组以grp间隔 分开,依次排序每一个分开的组
		for (int i = grp; i < n; ++i)
		{
			int end = i - grp;
			int key = arr[i];

			while (end >= 0 && key < arr[end])
			{
				arr[end + grp] = arr[end];
				end -= grp;
			}
			arr[end + grp] = key;
		}
		grp-=2;
	}
}

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

空间复杂度:O(1)

稳定性:不稳定

三、选择排序

思想:

从每次待排序的序列中选择出一个最小的元素,将其放在序列的起始位置,直到全部元素排列完

// 选择排序
void SelectSort(int* arr, int n)
{
	//选择一个最小的值放在每次循环查找的第一位
	for (int i = 0; i < n-1; ++i)
	{
		//每次更新 minpos 的位置
		int minpos = i;
		//在第i个元素和第n个元素之间找一个最小值
		for (int j = i; j < n; ++j)
		{
			if (arr[minpos]>arr[j])
			{
				minpos = j;
			}
		}
		//将最小值与第i个元素交换
		if(minpos!=i)
		{
			Swap(&arr[i], &arr[minpos]);
		}
	}
}

时间复杂度:

最坏情况:O(N^2)                       最好情况:O(N^2)

空间复杂度: O(1)

稳定性:不稳定

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

四、堆排序

思想:

利用堆的思想进行排序,总共分为俩个步骤:

1.建堆

升序建大堆,降序建小堆

建堆的时候要用到一种向下调整的方法来实现,具体实现看以下代码:

// 堆排序
//向下调整
void AdjustDwon(int* arr, int n, int root)
{
	int child = 2 * root + 1;
	while (child < n)
	{
		//右孩子存在且左孩子的值小于右孩子
		//交换左右孩子
		if (child + 1 < n && arr[child] < arr[child + 1])
		{
			Swap(&arr[child], &arr[child + 1]);
		}
		//双亲的值比左孩子小
		if (arr[root]<arr[child])
		{
			//交换双亲和左孩子
			//更新双亲和左孩子,使双亲指向左孩子,左孩子指向此时双亲的左孩子
			Swap(&arr[child], &arr[root]);
			root = child;
			child = 2 * root + 1;
		}
		else
		{
			return;
		}
		
	}
}
void HeapSort(int* arr, int n)
{
	//升序建大堆,降序建小堆
	//以下为建大堆,进行升序序排序
	for (int i = (n - 2) / 2; i >= 0; --i)
	{
		AdjustDwon(arr, n, i);
	}

	//每次循环将堆顶值和堆尾的值进行交换,在进行向下调整
	while (n > 0)
	{
		Swap(&arr[0], &arr[n - 1]);
		n--;
		AdjustDwon(arr, n, 0);
	}
}

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

空间复杂度:O(1)

稳定性:不稳定

五、冒泡排序

思想:

从数组的开始位置俩俩元素相比较,前一个比后一个大则交换俩元素位置,否则,继续向后比较

第一趟比较完,可以将数组中最大的元素放在数组结尾位置

依次循环比较n-1趟即可(n为数组元素个数)

// 冒泡排序
void BubbleSort(int* arr, int n)
{
	for (int i = 0; i < n - 1; ++i)
	{
		for (int j = 0; j < n - 1 - i; ++j)
		{
			if (arr[j] > arr[j + 1])
			{
				Swap(&arr[j], &arr[j + 1]);
			}
		}
	}
}

时间复杂度:

最好情况:O(N)                        最坏情况:O(N^2)

空间复杂度:O(1)

稳定性:稳定

六、快速排序

思想:

任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

快速排序寻找基准值下标有三种方法:

法一:hoare版本

思想:

1.选中数组中最后一个元素作为基准值

2.确定俩个指针,左指针指向数组最左边的元素,右指针指向数组结尾元素(刚开始右指针就指向基准值的位置)

3.从左指针指向的位置开始,依次与基准值进行比较,如果左指针指向的元素比基准值小,则左指针向后移动一位,否则,停止移动;左指针停止移动后,开始从右指针指向的位置开始比较,如果右指针指向位置的元素比基准值大,则右指针向左移动一位,否则,停止移动

4.循环上述步骤,直到左右指针相遇

5.返回左指针或者右指针位置下标

int PartSort1(int* arr, int left, int right)
{
	//找到最后一个元素作为基准值
	int key = arr[right - 1];
	int begin = left, end = right - 1;
	while (begin < end)
	{
		//循环存begin位置开始查找比基准值key大的元素
		while (begin < end && arr[begin] <= key)
		{
			begin++;
		}
		//从end位置开始查找比基准值key小的元素
		while (begin < end && arr[end] >= key)
		{
			end--;
		}
		//如果begin<end,则交换元素
		if (begin < end)
		{
			Swap(&arr[begin], &arr[end]);
		}
	}
	//上述循环退出,将基准值放在begin位置
	if (begin != right - 1)
	{
		Swap(&arr[begin], &arr[right - 1]);
	}
	//返回基准值的位置
	return begin;
}
//快速排序
void QuickSort(int* arr, int left, int right)
{
	//区间中只有一个元素
	if ((right - left) <= 1)
	{
		return;
	}
	else
	{
		//找到基准值位置p,基准值左侧元素都比基准值小,基准值右侧元素都比基准值大
		int p = PartSort1(arr, left, right);
		//递归排序基准值左侧 和 右侧
		QuickSort(arr, left, p);
		QuickSort(arr, p + 1, right);
	}
}

法二:挖坑法

思想:

挖坑法思想整体与第一种hoare相同

1.选中数组中最后一个元素作为基准值

2.确定俩个指针,左指针指向数组最左边的元素,右指针指向数组结尾元素(刚开始右指针就指向基准值的位置)

3.从左指针位置开始,依次向后与基准值进行比较,遇到比基准值大的,停止后移左指针,然后将此时左指针位置的元素放入右指针位置,在使右指针向前移动一位,此时左指针的位置就可以看为一个坑;然后从右指针开始继续与基准值进行比较,遇到比基准值小的元素停止移动,然后将此时右指针对应位置的元素,放入刚才左指针的位置,即将左指针对应的坑填满。

4.循环上述步骤,直到左右指针相遇

5.返回左指针或者右指针位置下标

// 快速排序挖坑法
int PartSort2(int* arr, int left, int right)
{

	int begin=left, end = right - 1;
	int key = arr[right - 1];

	while (begin < end)
	{
		while (begin < end && arr[begin] <= key)
		{
			begin++;
		}
		if (begin < end)
		{
			arr[end] = arr[begin];
			end--;
		}
		while (begin < end && arr[end] >= key)
		{
			end--;
		}
		if (begin < end)
		{
			arr[begin] = arr[end];
			begin++;
		}
	}
	arr[begin] = key;
	return begin;
}
//快速排序
void QuickSort(int* arr, int left, int right)
{
	//区间中只有一个元素
	if ((right - left) <= 1)
	{
		return;
	}
	else
	{
		//找到基准值位置p,基准值左侧元素都比基准值小,基准值右侧元素都比基准值大
		int p = PartSort2(arr, left, right);
		//递归排序基准值左侧 和 右侧
		QuickSort(arr, left, p);
		QuickSort(arr, p + 1, right);
	}
}

法三:双指针法

具体实现步骤如下:

// 快速排序前后指针法
int PartSort3(int* arr, int left, int right)
{
	//定义前后指针,指向left和left之前的位置
	int cur = left;
	int pre = cur - 1;
	int key = arr[right - 1];

	while (cur < right)
	{
		//如果当前元素值小于key && pre的下一个位置不等于cur
		//说明此时pre和cur之间的元素都比 key 大,当前cur的元素比key小
		//交换当前位置和pre位置的元素
		if (arr[cur]<key && ++pre != cur)
		{
			Swap(&arr[cur], &arr[pre]);
		}
		//使当前位置向后移动
		cur++;
	}
	if (++pre != right - 1)
	{
		Swap(&arr[pre], &arr[right - 1]);
	}
	return pre;
}
//快速排序
void QuickSort(int* arr, int left, int right)
{
	//区间中只有一个元素
	if ((right - left) <= 1)
	{
		return;
	}
	else
	{
		//找到基准值位置p,基准值左侧元素都比基准值小,基准值右侧元素都比基准值大
		int p = PartSort3(arr, left, right);
		//递归排序基准值左侧 和 右侧
		QuickSort(arr, left, p);
		QuickSort(arr, p + 1, right);
	}
}

快速排序总结

时间复杂度:

最好情况:O(NlongN)                            最坏情况:O(N^2)

空间复杂度:O(logN)

稳定性:不稳定

七、归并排序

思想:

归并排序 是建立在归并操作上的一种有效的排序算法 , 该算法是采用分治法的一个非常典型的用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有 序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
具体步骤:
void MergeSortData(int* arr, int left, int mid,int right, int* temp)
{
	int begin1 = left, end1 = mid;
	int begin2 = mid, end2 = right;

	//index为每次递归进来temp的下标
	int index = left;
	//依次比较左右两部分的元素大小,将较小的放入temp中
	while (begin1 < end1 && begin2 < end2)
	{
		if (arr[begin1] <= arr[begin2])
		{
			temp[index++] = arr[begin1++];
		}
		else
		{
			temp[index++] = arr[begin2++];
		}
	}

	//右部分比较完,左半部分未比较完
	while(begin1 < end1)
	{
		//交左半部分剩余的元素放入temp中
		temp[index++] = arr[begin1++];
	}
	//同理
	while(begin2 < end2)
	{
		temp[index++] = arr[begin2++];
	}

}
void _MergeSort(int* arr, int left, int right, int* temp)
{
	//区间中只有一个元素
	if ((right - left) <= 1)
	{
		return;
	}
	else
	{
		int mid = left + ((right - left) >> 1);
		//递归排序左右俩测
		//左侧[left,mid)
		_MergeSort(arr, left, mid, temp);
		//右侧[mid.right)
		_MergeSort(arr, mid, right, temp);
		//将排好序的左右两侧 进行合并
		MergeSortData(arr, left, mid, right, temp);
		//将合并好的元素拷贝到原数组
		memcpy(arr + left, temp + left, (right - left) * sizeof(int));
	}
}

 归并排序总结

时间复杂度:O(NlongN)

空间复杂度:O(N)

稳定性:稳定

八、计数排序

计数排序在数据范围集中时,效率很高

步骤:

1.找到数组中最大元素max和最小元素min

2.申请空间数组Count,统计数组中每个元素出现的次数(Count(arr[i]-min)即为元素arr[i]出现的次数)

3.在数组Count中,Count[0]表示 arr数组中最小的元素出现的次数,Count[1]表示 arr数组中次小的元素出现的次数(这里可能并不是下标1,因为次小的元素与min的差值可能大于1).......依次类推

4.对每个元素进行回收,Count[i]不为0 的情况下,说明在arr数组中存在元素i+min,且出现Count[i]次,回收元素时候,依次从Count数组中不为0的元素中 最小的下标开始回收。

// 计数排序
void CountSort(int* arr, int n)
{
	//找到最大值 和 最小值
	int min = arr[0], max = arr[0];
	for (int i = 1; i < n; ++i)
	{
		if (min > arr[i])
		{
			min = arr[i];
		}
		if (max < arr[i])
		{
			max = arr[i];
		}
	}
	//以上数组中的元素处于 min~max这个区间内
	//申请max-min+1个空间统计数组arr中每一个元素出现的次数
	int rang = max - min + 1;
	//申请空间,并全部初始化为0
	int* Count=(int*)calloc(rang,sizeof(int));

	//统计每个元素出现的次数
	for (int i = 0; i < n; ++i)
	{
		Count[arr[i] - min]++;
	}

	//对每个元素进行回收
	int size = 0;
	for (int i = 0; i < rang; ++i)
	{
		//在Count数组元素不为0的情况下
		//Count数组下标从小到大 依次是 arr数组中从小到大的元素出现的次数
		while (Count[i] > 0)
		{
			arr[size++] = i + min;
			Count[i]--;
		}
	}
	free(Count);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值