排序算法简介

一、插入排序

        插入排序是每次将一个待排序的记录按其关键字大小,插入前面已经排列好的子序列,直到全部记录插入完毕,此时序列有序。

1.直接插入排序

        按照插入排序的思想,那么最直观简单的算法就是直接插入排序了。

        直接插入排序分三个步骤:

        1)查找出前面已经排好序的子序列中待排序记录的位置;

        2)将该位置以后,待排序记录位置以前的元素向后挪一位;

        3)将待排序记录插入该位置。

        那么具体实现的代码如下:

void InsertSort(int* a, int n)
{
	int i = 0;
	int end = 0;
	for (i = 1; i < n;++i)
	{
		end = i-1;
		int tmp = a[i];
		while (end >= 0)
		{
			if (a[end] > tmp)
			{
				a[end + 1] = a[end];//查找位置并将元素向后挪
				end--;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = tmp;//插入待排序记录
	}
}

         直接插入排序,如果是在顺序存储的线性表中进行排序,在查找的过程中可以考虑优化为折半查找,再整体向后挪。尽管此时时间复杂度仍然为O(N^2),但是在数据量不大的排序表中,采用折半插入查找的性能较高。

2.希尔排序

        希尔排序基本思想也是插入排序,但是差别在于希尔排序会将序列分割为按特定长度进行跳跃的序列,例如:将{9,8,7,6,5,4,3,2,1}序列分割为:{9,6,3}、{8,5,2}、{7,4,1}三个子序列,将这三个子序列分别进行插入排序,然而子序列所在的相对位置并没有变,即:{3,2,1,6,5,4,9,8,7},也就是分割的序列中的元素排列后所在位置,只可能出现在序列中元素在原序列中出现过的位置。例如:3,6,9只出现在原序列0,3,6号三个位置,那么排列后3,6,9也只会在0,3,6号这三个位置。

        具体代码实现如下:

void ShellSort(int* a, int n)
{
	int i = 0;
	int end = 0;
	int gap = n;//此为子序列各元素间在原序列中的距离
	while (gap > 1)
	{
		gap = gap / 3 + 1;//最后+1保证最后一次排序是直接插入排序
		for (i = 1; i < n - gap + 1;i++)//此处可直接i++
		{
			end = i - gap;
			int tmp = a[i];
			while (end >= 0)
			{
				if (a[end] > tmp)
				{
					a[end + gap] = a[end];//注意+gap
					end = end - gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}
	}
}

        希尔排序通过分割序列,使得一些较大的元素可以以更快的速度移动到原序列的后面,通过几次分割排序,让整个序列相对有序,再使用直接插入排序,可以优化插入排序的性能。希尔排序的时间复杂度较难计算一般认为约为O(N^1.3),较直接插入排序性能较好。

二、交换排序

        交换排序是比较两个元素的关键字,根据比较结果来对换两个元素在序列中的位置。

        1.冒泡排序

        冒泡排序的基本思想是:从前往后(或从后往前)两两比较相邻元素的值,若为逆序则交换它们,直到序列比较完,此为第一趟冒泡。每次冒泡都能确定一个元素的最终位置,那么最多N趟冒泡就能完成排序,因此冒泡排序的时间复杂度为O(N^2)。代码如下:

void BubbleSort(int* a, int n)
{
	int i = 0;
	int j = 0;
	for (i = n - 1;i > 0;--i)
	{
		bool flag = 0;//设置这个变量记录此次冒泡是否发生了交换
		for (j = 0;j < i;++j)
		{
			if (a[j] > a[j + 1])
			{
				flag = 1;
				swap(&a[j], &a[j + 1]);
			}
		}
		if (flag == 0)//若此次冒泡未发生交换,说明序列已经有序可直接跳出
		{
			break;
		}
	}
}

        在冒泡排序过程中,我们可设置一个变量来记录此次排序是否发生交换,若未发生交换说明序列已经有序可直接跳出,由此增加冒泡排序的性能。

2.快速排序

        快速排序采用分治的方法,首先随机选取序列中一个元素作为基准(通常取首元素),通过一趟排序将序列划分为大于基准的部分和小于基准的部分,此为一次划分,然后再分别对子表进行同样的操作,直至每个部分只有一个元素或为空,即所有元素在其最终的位置上。而划分的过程需要结合代码进行理解,那么快速排序的代码如下:

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

        这段代码就是快速排序分而治之的过程,中间的PartSort1就是划分的过程,如下:

int PartSort1(int* a, int left, int right)
{
	int tmp = left;
	while (left < right)
	{
		//确保交换不会出错,应先找小再找大
		while (left < right && a[right] >= a[tmp])
		{
			right--;
		}
		while (left < right && a[left] <= a[tmp])
		{
			left++;
		}
		swap(&a[left], &a[right]);
	}
	swap(&a[left], &a[tmp]);
	return left;
}

        首先,确定一个基准tmp,然后进入循环,第一步就是从后向前逐一排查,直到找到小于基准的值,然后再从前向后逐一排查,直到找到大于基准的值。此时拥有了二者的位置,直接进行交换,完成以后便可开始下一轮循环。

        这一次循环做的事情,就是在序列右边找一个小于基准的值,序列的左边找一个大于基准的值,进行交换,从而向左边均小于基准,右边均大于基准的目标靠近。

        在最后left和right快要相遇时,会有两种情况,其一,left不动,right--,使left==right,由于right先动,此时的left经过上一轮交换此时所指的值一定小于基准,那么此时left和right所指的值一定小于基准值;其二,right不动,left++,使right==left,由于right先动,此时right经过排查一定指向了一个小于基准的值,那么left和right同时指向小于基准的值。

        因此,不论哪种情况,只要是right先动,最后left和right相遇时,指向的一定的小于基准的值,此时,将基准值与该位置的值进行交换,那么基准左边的值就均小于基准,右边的值均大于基准,一次划分就结束了。若读者还是不明白,可通过画图进行研究。

        此外,划分的方法除了以上这种外,还有挖坑法和双指针法,有时间再更(诶嘿)。

        快速排序也可以使用非递归的方法进行,此时需要借助栈,而思想与二叉树的层序遍历类似,区别在于层序遍历用到的是队列,这里使用的是栈。

三、选择排序

        选择排序是每次在待排序列中选择一个最小或最大的元素放在有序序列的后面或前面,直至待排序列只剩一个元素。

1.简单选择排序

        此排序每次遍历待排序列,选择其中最小的元素确定其位置。该排序的时间复杂度不受序列元素初始顺序的影响。代码实现如下:

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

2.堆排序

        首先要知道什么是堆,堆是一颗完全二叉树,若为大根堆,则其最大元素放在其根节点,且其任意一个非根结点的值小于或等于其双亲结点值。小根堆则正好相反,其最小值放在其根节点。

        堆排序就是利用堆的特点,将堆建成以后,取其根结点的值,然后用堆底的元素取代根结点的位置,再经过调整形成新的堆,再取其根结点的值,循环往复,直至堆中只剩一个元素。

        那么堆排序就需要两个操作,一个是建堆,一个是恢复堆。

void HeapSort(int* a, int n)
{
	int i = 0;
	for (i = (n-1) / 2;i >= 0;--i)//从最后一个非子叶结点开始向上进行调整建堆
	{
		AdjustDown(a, n, i);
	}
	//建堆

	for (i = n - 1;i > 0;i--)
	{
		swap(&a[0], &a[i]);//交换堆顶和堆底的元素,使每次最大值都放在有序序列最前,形成升序
		AdjustDown(a, i, 0);//仅需要对堆顶元素进行调整即可
	}
}
void AdjustDown(int* a, int n, int root)
{
	int child = root * 2 + 1;;
	while (child < n)
	{
		if (child + 1 < n && a[child] < a[child + 1])//此时为大根堆,因此&&后是小于号
		{
			child = child + 1;
		}
		if (a[root] < a[child])//同上,大根堆,因此小于号
		{
			swap(&a[root], &a[child]);
		}
		root = child;
		child = root * 2 + 1;
	}
}

        如需升序序列,需要建立大根堆,相反需要降序序列则建立小根堆。

        另外,如果遇到topK的问题时,可考虑使用堆排序的方式,例如,要在一百万个数据里选取最大的10个数,那么只需建立一个大小为10的小根堆,每次将堆顶的元素与数据中的元素进行比较,若堆顶元素小于该元素,则将该元素放入堆顶,再调整小根堆,然后再进行堆顶元素和数据的比较,循环往复,直至遍历完数据,那么小根堆中的10个数据即为最大的十个数据。

四、归并排序、计数排序

1.归并排序

        归并排序是将两个或两个以上的有序表归并成为一张有序表。例如,8个元素,每个元素即为一张有序表,两两归并后,产生4张有序表,再两两归并两次后,就得到一张有序表。归并排序需要一个辅助数组。

void Merge(int* a, int left, int right, int* tmp)
{
	if (left == right)
	{
		return;
	}
	int mid = (left + right) / 2;
	Merge(a, left, mid,tmp);
	Merge(a, mid + 1, right, tmp);

	int l1 = left, r1 = mid;
	int l2 = mid + 1, r2 = right;
	int i = left;

	while (l1 <= r1 && l2 <= r2)
	{
		if (a[l1] <= a[l2])
		{
			tmp[i++] = a[l1++];
		}
		else
		{
			tmp[i++] = a[l2++];
		}
	}
	while (l1 <= r1)
	{
		tmp[i++] = a[l1++];
	}
	while (l2 <= r2)
	{
		tmp[i++] = a[l2++];
	}
	memcpy(a + left, tmp + left, sizeof(int) * (right - left + 1));
}

void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	Merge(a, 0, n - 1, tmp);
	free(tmp);
}

2.计数排序 

        计数排序,遍历过一遍序列后,统计其中每次元素出现的频率,然后根据该频率生成一张有序表,此为计数排序。计数排序虽然是排序,但是其思想已经不涉及数据的比较。

        其有一个缺点在于,当数据跨度较大时,容易造成空间的浪费。

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;//形成一个相对范围,减少空间的浪费
	int* tmp = (int*)malloc(sizeof(int) * range);
	if (tmp == NULL)
	{
		perror("malloc fail!");
		return;
	}
	memset(tmp, 0, sizeof(int) * range);
	for (int i = 0;i < n;++i)
	{
		tmp[a[i] - min]++;
	}

	int j = 0;
	for (int i = 0;i < range;++i)
	{
		int k = tmp[i];
		while (k > 0)
		{
			a[j++] = min + i ;
			k--;
		}
	}
}

        最后还有一个基数排序,这里仅介绍其思想,例如,根据身份证号进行排序时,建立0-9十个队列,然后先根据身份证号最后一位插入相应队列中,然后收集队列中的数据形成一个序列,此时序列中各元素的最后一位从0开始向9递增。

        然后再根据该序列的顺序,按身份证号倒数第二位插入相应队列,然后再从0到9依次收集元素,此时,各元素按倒数第二位从0-9依次递增,同时,这时由于第一次的排列和收集,倒数第二位相同的元素,其最后一位一定是按从小到大进行排列的。

        那么经过18次分配收集以后,就能得到从小到大排列的有序序列。注意:要建立的队列的数量,与每个基数的数量有关,而与数字的位数无关,例如:身份证号每位有10个数字,因此建立10个队列,但是身份证号总共有18位,说明要经过18次分配和收集,而不是建立18个队列。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值