数据结构初阶:排序大法

系列文章目录

数据结构初阶:树初阶


前言

排序对于程序员对数据结构的理解和运用都有很大的提升,我们今天所学在以后的工作事件中可以有着很多的应用,例如游戏中一键整理背包物品,网页中搜索内容的排序啊,这些都是排序的应用,也许他们的逻辑更加复杂,但底层的逻辑仍然是初阶排序

一、排序的类型有哪些

排序的方法有种种,我们只介绍最为基础的几种

1.冒泡排序

2.选择排序

3.插入排序

4希尔排序

5.归并排序

6.堆排序

7.快速排序

二、具体如何实现

1.冒泡排序

1.名词解释

所谓冒泡排序,顾名思义,就是像冒泡泡一样,一轮排序选出一个目标数字,可以是大也可以是小,看目标是升序还是降序。

2.代码实现

void BubbleSort(int* a, int n)
{
	for(int i = 0;i<n-1;i++)
	{
		for(int j = 0;j<n-1-i;j++)
		{
			if(a[j]>a[j+1])
			{
				swap(&a[j],&a[j+1]);
			}
		}
	}
}

解释:升序为例,我们要模拟冒泡泡的过程,就是需要相邻两个数的连续比较,这样才能让较大数字"浮起来",也就是到数列的最末尾,而又需要将n个数据拍成有序,就需要n-1次循环实现(最后一个数自然有序),所以代码中选择了两层循环嵌套,外层控制排序的轮次,内层控制一轮比较的逻辑(前一个数比后一个数大就交换)

3.复杂度分析

由于双层循环的嵌套,并且内层和外层都要经历大概n次循环,所以其时间复杂度为O(n^2),在数据较多时性能并不优秀

2.选择排序

1.名词解释

何为选择,以打牌为例,这种情况相当于我们在摸牌时不看,先接到手里,最后一把拿起来看,(以升序为例)然后每次选择最大的放在右手边,然后依次理顺排序

2.代码实现

void SelectSort(int* a, int n)
{
	for(int i = 0;i<n/2;i++)
	{
		int mini = 0+i;
		int maxi = n-1-i;
		for(int j = i+1;j<n-i-1;j++)
		{
			if(a[j]<a[mini])
			{
				mini = j;
			}
			if(a[j]>a[maxi])
			{
				maxi = j;
			}
		}
		swap(&a[i],&a[mini]);
		swap(&a[n-1-i],&a[maxi]);
	}
}

解释:为了尽可能减少复杂度,我们一轮循环选出最大和最小两个数,分别放于数列末尾和开头,实现逻辑非常简单不再赘述 

3.复杂度分析

由于我们一次循环选择两个数排序,计算复杂度大概在n*n/2,也属于O(n^2)数量级

3.插入排序

1.名词解释

仍然以打牌为例,插入排序就像是我们常规的摸牌,摸一张牌,就把排放在其对应的位置上,插入排序的逻辑也是如此,每一次将新的元素放在已有的有序数组中从而完成排序

2.代码实现

void InsertSort(int* a, int n)
{
	for(int i = 1;i<n;i++)
	{
		int tmp = a[i];
		int j = i;
		while(j>0&&a[j-1]>tmp)
		{
			a[j] = a[j-1];
			j--;
		}
		a[j] = tmp;
	}
}

解释:我们仍然使用两个循环嵌套的模式来模拟插入排序的过程,外循环中 i 表示要插入的元素的下标,因为一个数自然有序,所以 i 从1开始,内循环控制比较的次数,确定要插入的新元素的位置

3.复杂度分析

插入排序,复杂度也在O(n^2)数量级上

4.希尔排序

1.名词解释

希尔排序时插入排序的升级版本,再插入排序中我们只能对相邻元素的大小进行比较,这就导致了在排序后期如果出现比较小的数字,就需要经过多次交换才能够到达对应的位置,希尔排序通过使用一个gap变量,很好的将数据跳跃的距离放大了,能够将大小相差不大的数据放在一起,减少了交换的次数

 2.代码实现

void ShellSort(int* a, int n)
{
	int gap = n/3;
	while(gap>0)
	{
		for(int i = gap;i<n;i+=gap)
		{
			int tmp = a[i];
			int j = i;
			while(j>0&&a[j-gap]>tmp)
			{
				swap(&a[j],&a[j-1]);
				j-=gap;
			}
			a[j] = tmp;
		}
		gap/=2;
	}
	
}

解释:希尔排序和插入排序关系紧密,多出了gap变量来调节步长,让数组先进性粗略的排序,将大小相差不大的放在一起,随着gap减小,数组变得更加有序,每一个循环的过程和插入排序类似,不多赘述。 

3.复杂度分析

希尔排序平均复杂度约为O(n^1.3)

5.归并排序

1.名词解释

归并分为两步,递归和合并,通过递归每次将数组二分,通过合并将两侧已经有序的数组合并

2.代码实现

void MergeSort(int* a, int left,int right)
{
	if(left >= right)
	return;
	int mid = (left+right)>>1;
	MergeSort(a,left,mid);
	MergeSort(a,mid+1,right);
	int tmp[right-left+1];
	int i = 0;
	lefti = left;
	righti = mid+1;
	while(lefti<=mid&&righti<=right)
	{
		if(a[lefti]<=a[righti])
		{
			tmp[i++] = a[lefti++];
		}
		if(a[lefti]>a[righti])
		{
			tmp[i++] = a[right++];
		}
	}
	while(lefti<=mid)
	{
		tmp[i++] = a[lefti++];
	}
	while(righti<=right)
	{
		tmp[i++] = a[righti++];
	}
	for(int i = 0;i<left-right+1;i++)
	{
		a[left+i] = tmp[i];
	}
}

解释:前一部分没什么好说的,首先是递归返回条件,然后是递归排左半侧,递归排右半侧,在计算mid时使用了位操作数,左移一位意味着得到的数字除以二,更加快速,合并的过程先创建一个临时数组,比较已经有序的两个数组的头元素那个更小,就先放入那个元素,直至将两个数组中的元素都遍历完成 

3.复杂度分析

递归每次除以2 的机制使得复杂度为对数级,最终复杂度为O(n*logn)

6.堆排序

1.名词解释

默认大家已经对堆这种数据结构有一定的了解,不清楚的可以先行学习一下再来,需要清楚父亲节点和孩子节点下标的关系,向上调整和向下调整等知识

2.代码实现

void AdjustDwon(int* a, int n, int root)
{
	int parent = root;
	int child = root*2+1;
	
	while(child<n-1)
	{
		if(a[child]>a[child+1])
		{
			child++;
		}
		if(a[parent]>a[child])
		{
			swap(&a[parent],&a[child]);
		}
		int tmp = parent;
		parent = child;
		child = tmp*2+1;
	}
	if(n==2)
	{
		if(a[0]>a[1])
		swap(&a[0],&a[1]);
	} 
}

void HeapSort(int* a, int n)
{
	for(int i = n/2-1 ;i>=0;i--)
	{
		AdjustDwon(a,n,i);
	}
	
	for(int i = 0;i<n-1;i++)
	{
		swap(&a[0],&a[n-1-i]);
		AdjustDwon(a,n-1-i,0);
	} 
	
}

 解释:先使用向下调整建堆,这里构建大根堆,进行升序排序,向下调整建堆的关键是从倒数第一个非叶子节点开始,每多一个元素向下调整一次

3.复杂度分析

由于堆的数据结构特点,堆排序的复杂度也是O(n*logn)

7.快速排序

1.名词解释

好的那么我们进入本节的重难点,快速排序,听名字就知道这个排序强的可怕,重要程度也是不言而喻。一句话讲完快速排序的核心思想,就是找到 "轴" 的位置。每次选择一个元素为轴元素,比轴元素大的放一边,比轴元素小的放在另外一边,每次确定好一个轴的位置,直至数组有序

2.代码实现

快速排序的实现代码有多种,一 一为大家讲解,为了统一下面代码都排升序

(1)hoare版本
int PartSort1(int* a, int left, int right)
{
	int axle = left;
	while(left<right)
	{
		if(a[right]>=a[axle])
		{
			right--;
		}
		else if(a[left]<=a[axle])
		{
			left++;
		}
		else
		{
			swap(&a[left],&a[right]);
		}
	}
	swap(&a[axle],&a[right]);
	return right;
} 

void QuickSort(int* a, int left, int right)
{
	if(left >= right)
		return;
	int mid = PartSort1(a,left,right);
	QuickSort(a,left,mid-1);
	QuickSort(a,mid+1,right);

}

解释 :这个版本的快排思路如下,先从右侧开始找,右侧找到比轴值小的元素找到为止,然后左侧开始向右找比轴值大的元素,找到为止,然后交换两个数,重复此过程,知道左侧的下标和右侧的下标重合,最后交换重合的位置和轴元素(注意需要先将轴元素和最前面的元素交换)

(2)挖坑法
int PartSort2(int* a, int left, int right)
{
	int tmp = a[left];
	int index = left;
	while(left<right)
	{
		while(left<right && a[right]>=tmp)
		{
			right--;
		}
		a[index] = a[right];
		index = right;
		while(left<right && a[left]<=tmp)
		{
			left++;
		}
		a[index] = a[left];
		index = left;
	}
	a[index] = tmp;
	return index;	
}

void QuickSort(int* a, int left, int right)
{
	if(left >= right)
		return;
	int mid = PartSort2(a,left,right);
	QuickSort(a,left,mid-1);
	QuickSort(a,mid+1,right);

}

解释:这个版本的思路是先行将轴值拿出来,把原来的位置变成一个坑,然后从右侧开始向左找比轴值小的元素,找到后填入之前的坑中,自己的位置变成坑的位置,然后左侧向右找比轴值大的元素,同样的操作,直至左右相遇,将轴值填入坑中

(3)前后指针法
int PartSort3(int* a, int left, int right)
{
	int key = a[left];
	int prev = left;
	int cur = prev+1;
	while(cur<=right)
	{
		if(a[cur]<key)
		{
			prev++;
			if(prev!=cur)
				swap(&a[prev],&a[cur]);
			++cur;
		}
		else if(a[cur]>=key)
		{
			cur++;
		}
	}
	swap(&a[prev],&a[left]);
	return prev;
}
void QuickSort(int* a, int left, int right)
{
	if(left >= right)
		return;
	int mid = PartSort3(a,left,right);
	QuickSort(a,left,mid-1);
	QuickSort(a,mid+1,right);

}

 

 解释:这个方法的思路是用两个变量cur和prev来充当指针,起始时prev放在left,cur比prev大1

调整时,如果cur指向的值小于轴值,那么prev++并把prev和cur所指的值交换,如果cur所指的值大于轴值就只++cur,等到cur走到末尾,交换轴值和prev所指位置的值。这个过程中我们可以看到cur只有在遇到比轴值大的数的时候才会和prev拉开身位,等到prev找到比轴值小的值时,prev的下一个一定是比轴值更大的元素

(4)非递归实现快排

以上三种方法都是通过递归实现的,我们再进行递归操作时,如果数据量很大,递归深度过深,很容易出现爆栈的情况,这时候就不得不调整为非递归的形式 

在这里,我们使用栈这种数据结构来模拟递归的过程,其先入后出的特点很符合递归的特点

 栈的实现

typedef struct stack{
	int* a;
	int top;
	int size;
}stack;

void init_stack(stack* st)
{
	st->size = 4;
	st->a = (int*)malloc(sizeof(int)*st->size);
	st->top = 0;
}


void push_back(stack* st,int x)
{
	if(st->top >= st->size)
	{
		st->size *=2;
		st->a = (int*)realloc(st->a,sizeof(int)*st->size);
	}
	st->a[st->top++] = x;
}

bool stack_empty(stack*st)
{
	if(st->top==0)
	return true;
	else
	return false;
}

void stack_pop(stack* st)
{
	st->top--;
}

int stack_top(stack* st)
{
	return st->a[st->top-1];
}


void swap(int* a, int* b)
{
	int c = *a;
	*a = *b;
	*b = c;
}

 快排的实现(partsort3同先后指针法)

void QuickSortNonR(int* a, int left, int right)
{
	stack* st = (stack*)malloc(sizeof(stack));
	init_stack(st);
	push_back(st,right);
	push_back(st,left);
	while(!stack_empty(st))
	{
		int left_tmp = stack_top(st);
		stack_pop(st);
		int right_tmp = stack_top(st);
		stack_pop(st);
		
		int mid = PartSort3(a,left_tmp,right_tmp);
		if(mid+1<right_tmp)
		{
			push_back(st,right_tmp);
			push_back(st,mid+1);
		}
		if(mid-1>left_tmp)
		{
			push_back(st,mid-1);
			push_back(st,left_tmp);
		}
	}
}

 解释:先将右边界入栈,再把左边界入栈,之后进入循环,取两个边界放入单次排序的函数中,然后得到轴的下标,并再次入栈,直至栈为空

3.复杂度分析

在以上方法中我们都选择的是最左侧的值为轴值,这样是不合理的,如果排序的数列已经接近有序,就会不可避免地将大量数据排在同一侧,大大增加了复杂度,所以我们可以使用rand()函数进行随机选择轴值,这样可以应对更多种情况。

此外,在面对数据量较多时,可以在递归到一定程度时使用插入排序,进行优化,减少递归深度,因为在数据量较小时插入排序等排序方法和快速排序差异不明显,而对栈的开销更少,性能更高

三、复杂度稳定性的对比

对于稳定不稳定的讨论,举个例子,就是大小一样的元素他们的相对位置在排序前后会不会发生改变,发生改变就是不稳定的,不改变就是稳定的,大家可以尝试不同的例子进行体会

总结

好的,这节结束,对于基本的排序方法我们都能有一个大概的了解,但是别人说多讲多,都不如自己敲一敲代码尝试一下,只有真正靠自己写出来代码,知识才是落到实处,加油吧,希望能有所收获

图片来自网络

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值