数据结构--排序

目录

1.冒泡排序

2.插入排序

3.希尔排序

4.堆排序

5.选择排序

6.快速排序

1.hoare版本

2.挖坑法

3.前后指针法

4.非递归快速排序

7.归并排序

8.计数排序

9.总结 


1.冒泡排序

  冒泡排序应该是从C语言阶段就开始使用的最为简单的排序,其思想为单趟两两比较,将大的数放到最后,之后再对剩余的进行排序再放第二大的数。

动画演示:

//冒泡排序
void BubbleSort(int* a, int n)
{
    //n-1趟
	for (int i = 0; i < n-1; i++) 
	{
        //单趟  右边已经排好了顺序所以不用比较
		for (int j = 0; j < n - i-1; j++)
		{
			if (a[j] > a[j + 1])
			{
				Swap(&a[j], &a[j + 1]);
			}
		}
	}
}
  • 时间复杂度:O(N^2)
  • 空间复杂度:O(1)
  • 稳定性:稳定

2.插入排序

插入排序的单趟思想是一个元素依次与前面的元素进行比较,如果比前面的元素小就交换。

单趟演示:

那么总体思路就是从第二个元素开始排,以保证前面的元素一定是有序的。

动画演示:

//插入排序
void InsertSort(int* a, int n)
{
    //tmp储存要比较的数,因此到n-1为止,
	for (int i = 0; i < n - 1; i++)
	{
        //end为要排序元素的前一个元素的下标
		int end=i;
		int tmp = a[end + 1];
		while (end>=0)
		{
			if (tmp < a[end])
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}
        //将end的下一个位置给tmp
		a[end+1] = tmp;
	}
}
  • 时间复杂度:O(N^2)
  • 空间复杂度:O(1)
  • 稳定性:稳定

3.希尔排序

希尔排序是插入排序的plus版,其在排序之前要先进行预排序,通过使用gap将文件以gap间距分为gap组,之后再使用插入排序。预排序之后再进行一次插入排序。

gap的值是可以改的,上面是以3为例

gap越大,越不接近有序。

gap越小,越接近有序。如果gap==1就是直接插入排序。

//希尔排序
void ShellSort(int* a, int n)
{
    //这里进行了一些改进,使gap的值可以变化,并且gap的值可以为1
	int gap = n;
	while (gap>1)
	{
        //+1是为了防止出现gap==0
		gap = gap / 3 + 1;
        //分组不会每次都恰好,防止越界,在n-gap及之后的元素都已经分好组了
		for (int i = 0; i < n - gap; i++)
		{
			int end = i;
            //每组的元素间距是gap,故end+gap
			int tmp = a[end + gap];
			while (end >= 0)
			{
				if (tmp < a[end])
				{
					a[end + gap] = a[end];
					end-=gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}

	}
}

可以看出希尔与插入排序十分类似,如果gap==1,甚至下面的代码就是插入排序。希尔排序是插入排序的优化版本,其速度也比插入排序快很多

  • 时间复杂度:O(N^1.3)
  • 空间复杂度:O(1)
  • 稳定性:不稳定

4.堆排序

  堆排序是根据数据结构中的堆来设计的,利用堆删除思想来进行排序。

  建堆和堆删除中都使用向下调整,堆顶元素是当前堆中的最大值或最小值,将堆顶元素与堆中最后一个元素交换,然后将剩余元素重新调整成堆,再取出堆顶元素。重复上述步骤,直到所有元素都被取出,即完成了排序。

升序:建大堆

降序:建小堆

//堆排序
void Adjustdown(int* a, int size, int parent)
{
	int child = parent * 2 + 1;
	while (child<size)
	{
		if (child+1 <size && a[child+1] < a[child ] )  // <小堆      >大堆
		{
			child++;
		}

		if (a[child] < a[parent])	// <小堆      >大堆
		{
			swap(&a[parent], &a[child]);
			parent = child;
			child = child * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

void HeapSort(int* a, int n)
{
	//建堆
	for (int i = (n - 1 - 1) / 2; i >= 0; --i)
	{
		Adjustdown(a, n, i);
	}
	int end = n - 1;	
    //交换
	while (end>0)
	{
		swap(&a[0], &a[end]);
		Adjustdown(a, end,0);
		end--;
	}

}
  • 时间复杂度:O(N*logN)
  • 空间复杂度:O(1)
  • 稳定性:不稳定

5.选择排序

选择排序单趟选出最小的元素,再与选择的元素位置交换

总体就是左边是已经排好序的,右边在依次排序。整体十分简单。因此我们加上一些优化,左右两边都开始,把小的放在左边,大的放在右边,

//选择排序
void SelectSort(int* a, int n)
{
	int begin = 0;
	int end = n - 1;
	
	while (begin<=end)
	{
        //mini记录最小元素的位置 maxi记录最大元素的位置
		int mini = begin;
		int maxi = begin;
		for (int i = begin; i <= end; i++)
		{
			if (a[i] < a[mini])
			{
				mini = i;
			}
			if (a[i] > a[maxi])
			{
				maxi = i;
			}
		}
        //把最小元素放到左边
		Swap(&a[begin], &a[mini]);
        //判断最大元素是否在最左边,如果在就更新位置
		if (maxi == begin)
		{
			maxi = mini;
		}
        //把最大元素放到右边
		Swap(&a[maxi], &a[end]);
		begin++;
		end--;
	}
}
  • 时间复杂度:O(N^2)
  • 空间复杂度:O(1)
  • 稳定性:不稳定

6.快速排序

  它敢这么叫就一定有它的原因

  快排的思想是取一元素为基准,将其余元素小的放在它左边,大的放在它右边。使左右两边局部有序

1.hoare版本

这样就会有一些疑问,为什么相遇的地方一定是key要在的位置?那我们分析一下

相遇无非就两种,L去遇R,R去遇L

L去遇R:L去遇R,因为R是先走,所以说明此时R已经找到小了,并且L这一路上都没有大了。那么相遇的位置左边都是小的,右边都是大的,那么key的位置就找到了

R去遇L:R先走,这就说明R一路上都是比key大的元素,极端一点,LR在最左侧相遇,那么此时右边都比key大,那么key的位置也就在那

总结:

        右边找小,左边找大,且右边先走

 因为快排是递归左右区间,所以最好key不大不小,不然左右区间有的大,有的小,造成效率上的损失。因此在代码中加入了三数取中的优化

//三数取中
int GetMidi(int* a, int begin, int end)
{
	int midi = (begin + end) / 2;
	//begin end midi三个数选中位数
	if (a[begin] > a[midi])
	{
		if (a[midi] > a[end])
		{
			return midi;
		}
		else if (a[begin] > a[end])
		{
			return end;
		}
		else
			return begin;
	}
	else
	{
		if (a[begin] > a[end])
		{
			return begin;
		}
		else if (a[midi] < a[end])
		{
			return midi;
		}
		else
			return end;
	}
}
//快速排序
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
		return;
	int midi = GetMidi(a, begin, end);
	Swap(&a[midi], &a[begin]);
 
	int left = begin;
	int right = end;
	int keyi = begin;
	while (left < right)
	{
		//右边找小,右边先走
		while (left < right && a[right] >= a[keyi])
		{
			right--;
		}
		//左边找大
		while (left < right && a[left] <= a[keyi])
		{
			left++;
		}
		Swap(&a[left], &a[right]);
 
	}
    //找的了key的位置
	Swap(&a[left], &a[keyi]);
	keyi = left;
	//此时的数组可以划分为:[begin,keyi-1] keyi [keyi+1,end]
    //递归key的左右区间
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
 
}

  快排是通过递归左右区间来实现的,而递归就要注意防止栈溢出,所以为了防止栈溢出,可以将较小的区间交给其他排序来完成。

void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
		return;
	//小区间优化,小区间交由插入排序
	if (end -  begin + 1 < 10)
	{
		InsertSort(a + begin, end - begin + 1);
	}
	else
	{
		int midi = GetMidi(a, begin, end);
		Swap(&a[midi], &a[begin]);
 
		int left = begin;
		int right = end;
		int keyi = begin;
		while (left < right)
		{
			//右边找小
			while (left < right && a[right] >= a[keyi])
			{
				right--;
			}
			//左边找大
			while (left < right && a[left] <= a[keyi])
			{
				left++;
			}
			Swap(&a[left], &a[right]);
 
		}
		Swap(&a[left], &a[keyi]);
		keyi = left;
		//[begin,keyi-1] keyi [keyi+1,end]
		QuickSort1(a, begin, keyi - 1);
		QuickSort1(a, keyi + 1, end);
	}
}

小区间优化效果有,但不明显

2.挖坑法

  对比hoare版本的思想,挖坑法则更为顺畅。但核心思想还是不变。

挖坑法是在key处挖个坑位,R先走找小,把小放在坑中,此时坑位更新位置。再L找大,最后相遇后会有一个坑位,这便是key的位置

//挖坑法
int PartSort2(int* a, int begin, int end)
{
	int midi = GetMidi(a, begin, end);
	Swap(&a[midi], &a[begin]);
 
	int key = a[begin];
	int holei = begin;
 
	while (begin < end)
	{
		//右边找小
		while (begin < end && a[end] >= key)
		{
			end--;
		}
        //更新坑位的值和位置
		a[holei] = a[end];
		holei = end;
		//左边找大
		while (begin < end && a[begin] <= key)
		{
			begin++;
		}
		a[holei] = a[begin];
		holei = begin;	                                                                                                                                                  
	}
    //将key放到最后的值
	a[holei] = key;
 
	return holei;
}
 
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
		return;
	int keyi = PartSort2(a, begin, end);
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

3.前后指针法

这种方法是思想是,创建前后指针prev和cur,prev指针指向开始,cur指针指向prev指针的后一个位置

cur找小,找到就++prev,交换prev和cur位置的值,++cur

结束标志就是cur到达最右

// 快速排序前后指针法
int PartSort3(int* a, int begin, int end)
{
	int midi = GetMidi(a, begin, end);
	Swap(&a[midi], &a[begin]);
 
	int keyi = begin;
	int cur = begin + 1;
	int prev = begin;
 
	while (cur <= end)
	{
		//cur找小,prev先++再交换
		if (a[cur] < a[keyi] && prev++ != cur)
			Swap(&a[prev], &a[cur]);
		cur++;
	}
	Swap(&a[prev], &a[keyi]);
	keyi = prev;
 
	return keyi;
}
 
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
		return;
	int keyi = PartSort3(a, begin, end);
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

个人感觉前后指针法较为简单

4.非递归快速排序

  上面三种都是使用递归来实现的,那还是那个问题,万一要递归的很深,即便加上了小区间优化都栈溢出了怎么办呢?那就得用非递归来实现,非递归要借助栈和循环来实现

假如10个数据,那么0,9入栈,进行一次排序,排序后分为[begin,keyi-1] keyi [keyi+1,end]三部分,也就是[0,4] 5 [6,9]。那么再将0,4,6,9入栈,依次将区间分解。直到栈为空,也就结束了

//非递归快速排序
void QuickSortNonR(int* a, int begin, int end)
{
	ST s;
	STInit(&s);
    //记住栈的特性,因此要按相同的次序入区间的左右位置
	STPush(&s, end);
	STPush(&s, begin);
 
	while (!STEmpty(&s))
	{
		int left = STTop(&s);
		STPop(&s);
		int right = STTop(&s);
		STPop(&s);
 
		int keyi = PartSort3(a, left, right);
		//[left ,keyi-1] keyi [keyi+1,right]
        //判断左右区间长度是否<=1
		if (left < keyi - 1)
		{
			STPush(&s, keyi - 1);
			STPush(&s, left);
		}
		if (keyi+1 < right)
		{
			STPush(&s, right);
			STPush(&s, keyi+1);
		}
	}
}
  • 时间复杂度:O(N*logN)
  • 空间复杂度:O(logN)
  •  稳定性:不稳定

7.归并排序

  归并排序就是先将序列分解再依次比较合并,最终将排序好的序列拷贝回原数组。

//归并排序
void _MergeSort(int* a, int left, int right,int* tmp)
{
    //区间中元素<=1时不合并
	if (left >= right)
	{
		return;
	}
 
    //划分数组,每次一分为二  // [begin mid]  [mid+1 end]
	int mid = (left + right) / 2;
	_MergeSort(a, left, mid,tmp);//继续分解左区间
	_MergeSort(a, mid + 1, right,tmp);//继续分解右区间
 
	int begin1 = left, end1 = mid;//有序序列1
	int begin2 = mid + 1, end2 = right;//有序序列2
	int i = left;
	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)
{
    assert(a);
    //因为需要将两个有序序列合并,需借助额外数组
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc");
		exit(-1);
	}
 
	_MergeSort(a, 0, n - 1,tmp);
 
	free(tmp);
	tmp = NULL;
}
  •  归并排序因为需要额外的数组来存储排好的序列因此需要O(N)的空间复杂度
  •  时间复杂度:O(N*logN)
  •  空间复杂度:O(N)
  •  稳定性:稳定

8.计数排序

  计数排序与上面的排序都不太相同,因为其不需要交换元素,而是像它的名字一样通过计数的方式来排序

for(int i=0;i<n;i++)
{
    count[a[i]]++;
}

  通过计数来排序,将数的值和数组的下标结合,那些数出现了就将相应下标+1

计数排序效率极高,时间复杂度O(aN+countN(范围)),空间复杂度O(countN(范围))

局限性:

        1.不适合分散的数据,更适合集中数据

        2.不适合浮点数、字符串、结构体数据排序,只适合整数。

但如果要排的数较大,列如都是10000~99999的数,那么前0-9999就被浪费了,因此可以进行一些小优化。找到其最小,并从最小来进行相对映射

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;
    //calloc可以将数组初始化为0
	int* count = (int*)calloc(range, sizeof(int));
	if (count == NULL)
	{
		perror("calloc fail");
		return;
	}
	//统计次数
	for (int i = 0; i < n; i++)
	{
		count[a[i] - min]++;
	}
	//排序
	int i = 0;
	for (int j = 0; j < range; j++)
	{
		while (count[j]--)
		{
            //因为最小不是0,而是min,所以最后要加回来
			a[i++] = j + min;
		}
	}
 
}
  • 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
  •  时间复杂度:O(MAX(N,范围))
  •  空间复杂度:O(范围)
  • 稳定性:稳定 

9.总结 

 


如有错误,感谢斧正

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值