排序

本文详细介绍了选择排序、冒泡排序、插入排序、希尔排序、计数排序、快速排序(包括Hoare版本、挖坑法、前后指针法和三数取中优化)、以及归并排序的基本原理、优化方法和性能分析。着重讨论了这些排序算法的实现、优缺点以及适用场景。
摘要由CSDN通过智能技术生成

选择排序

选择排序是寻找数组中的最大值(最小值)的下标,然后交换。

void SelectSort(vector<int>& nums) {
    int mins = 0;
    for (int i = 0; i < nums.size() - 1; i++) {
        mins = i;
        for (int j = i; j < nums.size(); j++) {
            if (nums[j] < nums[mins]) {
                mins = j;
            }
        }
        swap(nums[i], nums[mins]);
    }
}

当然我们可以优化一下,上面原始做法是找最大值,其实我们可以分区间去找最小和最大,然后把最小值和该区间第一个数交换,最大值和该区间最后一个交换。

但是这样写会存在一个问题,就是当maxi的位置和begin的位置相同,mini的问题和end的位置相同,那么就会重复交换,相当于两次交换的同样的位置,那么没有起到交换的作用。

void SelectSort(vector<int>& nums) {
	int mini, maxi;
	int begin = 0;
	int end = nums.size() - 1;
	while (begin < end) {  // 区间为[begin, end]
		mini = maxi = begin;
		for (int i = begin + 1; i <= end; i++) {
			if (nums[i] < nums[mini]) {
				mini = i;
			}
			if (nums[i] > nums[maxi]) {
				maxi = i;
			}
		}
		swap(nums[mini], nums[begin]);
        // 如果maxi于begin位置重合,那么maxi的位置需要修正
        if(maxi == begin){
            maxi = mini;
        }
		swap(nums[maxi], nums[end]);
		++begin;
		--end;
	}
}

 选择排序其实效果很差,比插入还差。当数组有序的时候。

冒泡排序

void BubleSort(vector<int>& nums) {
        for(int i = 0; i < nums.size() - 1; i++){
            for(int j = 0; j < nums.size() - 1; j++){
                if(nums[j] > nums[j+1])
                    swap(nums[j], nums[j+1]);
            }
        }
}

插入排序

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

void InsertSort(vector<int>& nums) {
	for (int i = 0; i < nums.size() - 1; i++) {
		int end = i;
		int temp = nums[end+1];
		while (end >= 0) {
			if (temp >= nums[end]) { // 当已经大于,满足升序条件,不需要排
				break;
			}
			else { //当小于,不断往后移动元素,也就是找插入位置
				nums[end + 1] = nums[end];
				end--;
			}
		}
		nums[end + 1] = temp; // 找到了位置,就插入
	}
}

希尔排序

希尔排序就是在插入排序上进行优化,每一次的排序,都是把数组变得更加有序。把数组分成等差的,定义gap,将数组分成两部分,假如是升序排序,那么每一次比较前面部分的和后部分,假如前大于后,那么就进行插入,如果小于,就原地插入。假如gap 等于1,那么就已经排好了。 

与插入排序的比较:插入排序是把元素一个一个向后移,希尔排序是把元素向后移动gap个。

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

void ShellSort(int* arr, int size)
{
    int gap = size;
    while (gap > 1)
    {
        gap = gap / 3 + 1;	//调整希尔增量, 加1保证最后一次gap等与1
        for (int i = 0; i < size - gap; i++)	//从0遍历到size-gap-1
        {
            int end = i;
            int temp = arr[end + gap];
            while (end >= 0)
            {
                if (arr[end] > temp)
                {
                    arr[end + gap] = arr[end];
                    end -= gap;
                }
                else
                {
                    break;
                }
            }
            arr[end + gap] = temp;	//以 end+gap 作为插入位置
        }
    }
}

计数排序

计数排序本质上就是一个哈希,原始的做法就是开一个数组中最大值大小的数组,然后遍历待排序数组,通过映射(开辟的数组的下标等于待排序数组的元素,每遍历一次待排序数组的元素,就在开辟的数组对应映射的下标添加一次次数)。但是可以在这个基础上进行优化,缩小开辟数组的空间,假如按照原始方法去开辟数组大小,假如原始数组的最大值过大,那么会开辟一个空间很大的数组,那么就会造成空间浪费,空间复杂度很高。那么优化的思路就是降低这个开辟数组的大小,方法就是取待排序数组的最大值和最小值。最大值和最小值的差+1,就是开辟数组的大小。

void CountingSort(vector<int>& nums, int max_num, int min_num) {
	//max_num为待排序数组的最大值,min_num为待排序数组的最小值
	int size = max_num - min_num + 1; 
	vector<int> count(size);
	for(int i = 0; i < nums.size(); i++){
		count[nums[i] - min_num]++; 
        // 按照原始方法就是 count[nums[i]]++,但是优化处理做了一个简单的映射
	}
	int index = 0; 
	for (int i = 0; i < size; i++) {
		while (count[i] > 0) {
			nums[index++] = i + min_num;
            // 按照原始方法就是nums[index++] = i,但是优化处理做了一个简单的映射
			count[i]--; // 计数完一次,就减少次数
		}
	}
}

快速排序

顾名思义快排就是最快的。

我们这都是以升序来考虑

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

这一过程有三种方法实现:

1、Hoare版本
2、挖坑法
3、前后指针法

Hoare版本

对区间左右端点任取一个值作为key(基准值)。

比如取右端点作为key,从左端点往右找比key大的值,然后右端点往左找比key小的值。两边找到后就交换值,最后结束的时候,两个指针指向一个位置,然后将这个位置和key的位置交换。注意:这里先后顺序很重要。

假如取左端点作为key,从右端点往左找比key小的值,然后左端点往右找比key大的值。同样的,顺序也很重要

最后的单趟排序之后区间的情况:[前部分] [key的值] [后部分],前后的子序列中间的分割是key,前部分的值均小于后部分。

代码如下:

void PartSort_1(int* a, int left, int right)
{
    // 取右端点为key
	int key = right;
	while (left < right)
	{
		while (left < right && a[left] <= a[key])
		{
			++left;
		}
		while (left < right && a[right] >= a[key])
		{
			--right;
		}
		Swap(&a[left], &a[right]);
	}
	Swap(&a[right], &a[key]);
}
void PartSort_1(int* a, int left, int right)
{
    // 取左端点为key
	int key = left;
	while (left < right)
	{
		while (left < right && a[right] >= a[key])
		{
			--right;
		}
		while (left < right && a[left] <= a[key])
		{
			++left;
		}
		Swap(&a[left], &a[right]);
	}
	Swap(&a[right], &a[key]);
}

挖坑法

和第一个单次排序一样,需要先选定一个key做为坑。左右端点都可以,但是顺序和hoare版本一样。
 

void PartSort_2(int* a, int left, int right)
{
	// 第一个坑挖在右端点
	int key = a[right];
	int hole = right;
	while (left < right)
	{
		// 左边找大,填在右边坑
		while (left < right && a[left] <= key)
		{
			++left;
		}
		a[hole] = a[left];
		hole = left;
		// 右边找小,填在左边坑
		while (left < right && a[right] >= key)
		{
			--right;
		}
		a[hole] = a[right];
		hole = right;
	}
	a[hole] = key;
}
void PartSort_2(int* a, int left, int right)
{	
    int key = a[left];
	int hole = left;
	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;
    //return hole;
}

前后指针法

void PartSort_3(int* a, int left, int right)
{
	int prev = left;
	int cur = left + 1;
	int key = left;
	while (cur <= right)
	{
        // 寻找比key大的值
		if (a[cur] < a[key] && ++prev != cur)
		{
			Swap(&a[prev], &a[cur]);
		}
		cur++;
	}
	Swap(&a[key], &a[prev]);
    //return prev;
}

三数取中

为什么三数取中呢?当一个数组是已经有序的,假如选择第一个位置或最后一个位置的值,那么就找不到比该位置小或大的值,这样的话时间复杂度就达到了O(n^2)。那怎样选择才会规避这种情况呢?那么可以在这个区间呢取一个不是最大也不是最小的数,使用三数取中的方法就可以达到。

整体的代码实现 

当一个待排序的数组已经是有序的,那么快排的时间复杂度就会到O(n^2)。

在确定基准值key之前,先求一下待排序的靠中间的数,这样就会避免是最大或最小的数。这个函数实现是GetMidIndex

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

// Hoare版本的单趟排序
int PartSort_1(int* a, int left, int right)
{
	int mid = GetMidIndex(a, left, right);
	Swap(&a[right], &a[mid]);
	int key = right;
	while (left < right)
	{
		while (left < right && a[left] <= a[key])
		{
			++left;
		}
		while (left < right && a[right] >= a[key])
		{
			--right;
		}
		Swap(&a[left], &a[right]);
	}
	Swap(&a[right], &a[key]);
	return right;
    // return left 也可以因为单趟排序之后,最后right,left指向同一个地方
}

void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	int div = PartSort_1(a, begin, end);
	QuickSort(a, begin, div - 1);
	QuickSort(a, div + 1, end);
}

性能分析

时间复杂度:O(n * log n)

空间复杂度:O(log n)

递归实现的深度为log n。

快排的非递归实现

void QuickSortNonR(int* a, int begin, int end)
{
	stack<int> st;
	st.push(begin);
	st.push(end);
	while (!st.empty())
	{
		int right = st.top();
		st.pop();
		int left = st.top();
		st.pop();
		int key = PartSort_1(a, left, right);
		if (left < key - 1)
		{
			st.push(left);
			st.push(key - 1);
		}
		if (key + 1 < right)
		{
			st.push(key + 1);
			st.push(right);
		}
	}
}

递归和非递归的区别

使用递归会不断调用函数,而调用函数会创建函数栈帧,而快排使用递归需要分割区间到只有一个元素,假如数组非常大,就需要创建很多的栈帧,那么就会有栈溢出的风险,而使用非递归的方法,在堆上开辟空间,那么就可以规避这种风险。

栈的空间是M级别的空间(大概8M,根据不同的机器有不同的大小),而在堆区的空间是G级别的空间。

小区间优化

随着递归的深度不断深入,每一层的递归次数会以2倍的系数快速增加。为了减少递归的最后几层递归,我们可以增加一个判断语句,当元素的个数小于一定数量(小于15),就可以使用其他排序(比如希尔排序),这样就可减少递归的深度,在一定程度上可以优化快速排序的性能。待排序的数组越长,优化效果越明显。

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

// Hoare版本的单趟排序
int PartSort_1(int* a, int left, int right)
{
	int mid = GetMidIndex(a, left, right);
	Swap(&a[right], &a[mid]);
	int key = right;
	while (left < right)
	{
		while (left < right && a[left] <= a[key])
		{
			++left;
		}
		while (left < right && a[right] >= a[key])
		{
			--right;
		}
		Swap(&a[left], &a[right]);
	}
	Swap(&a[right], &a[key]);
	return right;
    // return left 也可以因为单趟排序之后,最后right,left指向同一个地方
}

void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
    if(end - begin + 1 < 15)
    {
        ShellSort(a + begin, end - begin + 1);
    }
    else
    {
	    int div = PartSort_1(a, begin, end);
	    QuickSort(a, begin, div - 1);
	    QuickSort(a, div + 1, end);
    }
}

归并排序

归并排序就是将一个待排序的数组不断分割,直到分割到只有一个元素,那么就进行合并。这个合并是合并在一个新开辟的数组temp上,然后在复制到原数组上。

void _MergeSort(int* a, int left, int right, int* temp)
{
	if (left >= right) // 当只有一个元素或没有元素就返回
	{
		return;
	}
	int mid = left + (right - left) / 2;
	_MergeSort(a, left, mid, temp); // 对左边归并
	_MergeSort(a, mid + 1, right, temp); // 对右边归并
	int begin1 = left, end1 = mid;
	int begin2 = mid + 1, end2 = right;
	int i = left;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] <= a[begin2])
		{
			temp[i++] = a[begin1++];
		}
		else
		{
			temp[i++] = a[begin2++];
		}
	}
	while (begin1 <= end1)
	{
		temp[i++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		temp[i++] = a[begin2++];
	}
	for (int index = left; index <= right; index++)
	{
		a[index] = temp[index];
	}
}

void MergeSort(int* a, int n)
{
	int* temp = new int[n];
	_MergeSort(a, 0, n - 1, temp);
}

非递归实现

归并排序的非递归实现并不需要借助栈来实现,只需要控制每次参与合并的元素个数即可。
可以使用一个gap来控制每次参与合并元素的个数,但是使用gap来控制元素个数,会产生一些区间错误。
情况一:
当最后一组区间合并时,第二个区间不存在,直接跳出循环,不在合并
情况二:
当最后一组区间的第一个区间元素不够gap个元素,直接跳出循环,不在合并
情况三:
当最后一组区间的第二个小区间的元素不够gap个元素,则将第二个小区间的右端点更新为待排序数组的右端点。
 

void _MergeSortNonR(int* a, int begin1, int end1, int begin2, int end2, int* temp2)
{
	int i = begin1;
	int j = begin1;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] <= a[begin2])
		{
			temp2[i++] = a[begin1++];
		}
		else
		{
			temp2[i++] = a[begin2++];
		}
	}

    // 当两个小区间合并完之后,有一个小区间的元素还有剩余,直接把它添加到temp数组之中
	while (begin1 <= end1)
	{
		temp2[i++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		temp2[i++] = a[begin2++];
	}
    
    // copy数组
	for (; j <= end2; j++)
	{
		a[j] = temp2[j];
	}
}

void MergeSortNonR(int* a, int n)
{
	int* temp2 = new int[n];
	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i += 2 * gap)
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;
            
            //规避区间错误,调整区间端点。
			if (begin2 >= n)
			{
				break;
			}
			if (end2 >= n)
			{
				end2 = n - 1;
			}
			_MergeSortNonR(a, begin1, end1, begin2, end2, temp2);
		}
		gap *= 2;
	}
}

性能分析

时间复杂度:O(n * log n)

空间复杂度:O   (n)

  • 5
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值