排序算法之直接插入排序、二分插入排序和希尔排序


下列所有排序都默认是升序,从小到大。

插入排序

有一个已经有序的数据序列,要求在这个已经排好的数据序列中插入一个数,但要求插入后此数据序列仍然有序,这个时候就要用到一种新的排序方法——插入排序法,插入排序的基本操作就是将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据,算法适用于少量数据的排序时间复杂度为O(n^2)。是稳定的排序方法。插入算法把要排序的数组分成两部分:第一部分包含了这个数组的所有元素,但将最后一个元素除外(让数组多一个空间才有插入的位置),而第二部分就只包含这一个元素(即待插入元素)。在第一部分排序完成后,再将这个最后元素插入到已排好序的第一部分中。

直接插入排序


插入排序的过程


插入排序就像有的人打扑克一样,每次摸起来一张牌后,在手中已经排序好的扑克中插入当前摸起的这张牌。问题的关键点就是:如何在已经排好序的序列中,找到当前元素该插入的位置


当序列中只有一个元素时,不需要排序,因为一个元素已经有序。


现在来考虑有多个元素的过程:




因为一个元素已经有序,所以需要从第二个元素开始找插入位置,即需要有一层循环从第二个元素遍历到最后一个元素。

然后来思考在循环内该怎么做?

首先我们应该知道,在一个已经就绪的序列中插入一个元素,肯定 会导致插入位置之后的所有元素都会向后移动一位, 要牢记这一点。

循环内:
我们可以先把当前把插入的元素先保存起来,然后分别将刚才保存起来的元素与有序序列中的每一个元素比较,那么比较到什么时候才停下来?
a. 出现一个比保存的元素小的元素(saveNum < array[ i ] );
b.以序队列已经比较完毕(i >=0 )。
因为以序队列的范围是从 0 .. 当前插入元素位置 -1 , 所以我们需要一个循环来完成比较和搬迁元素。
当循环条件不满足时,即找到插入位置。直接插入即可。

参考代码:

void InsertSort(int* array, int size)
{
	if (array == NULL || size <= 0)
		return;

	// 从数组第二个元素开始处理,因为一个元素已经有序
	for (int i = 1; i < size; i++)
	{
		int temp = array[i];
		int end = i - 1;
		
		// 插入array[i] 到已序序列中 array[0.. i-1]
		while (end >= 0 && temp < array[end])
		{
			array[end + 1] = array[end];
			end--;
		}

		// 插入 array[i]
		array[end + 1] = temp;
	}
}

// 打印数组
void PrintfArray(string info , int* array, int size)
{
	cout << info << ":";
	for (int i = 0; i < size - 1; ++i)
		cout << array[i] << " ";
	cout << endl;
}

int main()
{
	int array[] = {5, 9, 1, 6, 2, 4, 7, 8, 0 , 2, 0};
	int size = sizeof(array) / sizeof(array[0]);
	PrintfArray("排序前", array, size);
	InsertSort(array, size);
	PrintfArray("排序后", array, size);
	return 0;
}


输出:
排序前:5 9 1 6 2 4 7 8 0 2
排序后:0 0 1 2 2 4 5 6 7 8


二分插入排序


我们知道,插入排序最重要的就是在 已序序列中找到插入位置,在上面的代码中,我们每次找位置,都需要一个一个挨着遍历元素,如果插入位置在第以序序列的头部(从后往前比较),那么时间复杂度将是线性的,最坏情况O(N)。

细心的同仁们已经发现在以序序列,那么是不是可以把二分查找的思想搬迁过来,可以提高找位置的效率呢?     文章: 二分查找递归和循环实现

当然是可以的。

思路主要是利用在有序序列查找元素复杂度为对数级别,从而优化了 找元素的时间。但是搬移元素的时间是必不可少的。

参考代码:
void InsertSort_Binary(int * array, int size)
{
	if (array == NULL || size <= 0)
		return;

	for (int i = 1; i < size; ++i)
	{
		
		int temp = array[i];

		int left = 0;     // 有序序列的左边界
		int right = i - 1;// 有序序列的右边界

		while (left <= right)
		{
			int mid = (left + right) / 2;
			// 注意这里的 等号,为了保证算法的稳定性(相同关键字排序前后位置不会变)
			// 所以也需要向后移动
			if (array[mid] <= temp)
				left = mid + 1;
			else if (array[mid] > temp)
				right = mid - 1;
			
		}

		// 搬移数据, 上面循环结束后,left的位置就是插入位置
		// 需要将从left 到 当前插入元素的前一个位置都搬移一个位置
		for (int j = i - 1; j >= left; --j)
		{
			array[j + 1] = array[j];
		}
		array[left] = temp;
	}
}



希尔排序

希尔排序是插入排序的改良版,虽然可以使用二分来较少找元素插入位置的时间。但是主要的时间消耗都在搬移元素,而且一次只能搬移一个元素。而希尔排序是在指直接插入排序的基础上,添加了一个排序增量的方法。 把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。使用不同的希尔增量会有不同的改进效果。

希尔排序算法是直接插入排序算法的一种改进,减少了其复制的次数,速度要快很多。 原因是,当n值很大时数据项每一趟排序需要移动的个数很少,但数据项的距离很长。当n值减小时每一趟需要移动的数据增多,此时已经接近于它们排序后的最终位置。 正是这两种情况的结合才使希尔排序效率比插入排序高很多。

参考代码:
增量数值可参考网上数学分析过的数值,下面代码举例给了321。

void ShellSort(int*array , int size)
{
	int gap = 3;
	while (gap > 0)
	{
		for (int idx = gap; idx < size; idx += gap)
		{
			int temp = array[idx];

			int end = idx - gap;
			while (end >= 0 && array[end] > temp)
			{
				array[end + gap] = array[end];
				end -= gap;
			}
			array[end + gap] = temp;
		}
		gap--;
	}
}




总结一下: 如果目标是把n个元素的序列升序排列,那么采用插入排序存在最好情况和最坏情况。最好情况就是,序列已经是升序排列了,在这种情况下,需要进行的比较操作需(n-1)次即可。最坏情况就是,序列是降序排列,那么此时需要进行的比较共有n(n-1)/2次。插入排序的赋值操作是比较操作的次数加上 (n-1)次。平均来说插入排序算法的时间复杂度为O(n^2)。因而,插入排序不适合对于数据量比较大的排序应用。但是,如果需要排序的数据量很小,例如,量级小于千,那么插入排序还是一个不错的选择。

你可能感兴趣的:(练习,C/C++)