算法第三记-插入排序

     今天我们要讲的是插入排序,对于插入排序的思路我们可以拿我们平时打扑克整理牌的思路来讲,当我们拿到一张牌我们先随意放在左边,然后又抽到一张牌,那么这张牌应该放在哪里呢?一般来说我们会跟已经整理好的牌序列最右边的那张比较比较,如果比它小,我们就需要将牌插入到它的左边,然后继续不断比较,直到遇到比待插入牌小的位置,然后我们插入到它的后面。这样解释或许有点牵强,因为有时候我们打牌不只是会去凑顺子。之所以这样比喻,是因为好多算法书都喜欢用这个例子,所以我总结的时候也选择这个了。

     在一段模糊的比喻之后。我们来点更专业的术语。插入排序其实也是维护了一个有序序列和一个无序序列,排序的过程其实就是从未排序序列拿一个元素出来,然后在已排序序列中找到适合自己待的位置,不断重复....最终使得已排序序列逐渐增长到原先有序序列和无序序列的长度总和。

    还有一个重要的点就是在找待插入元素该待的位置时,已经比较过的元素需要向后挪,这是为了给待插入元素腾位置。如图示意

这是一整趟插入排序结束的过程,而且我们还需要注意在找待插入元素该待的位置时,我们最终停止的位置会比最终要插入的位置小1,因为我们找待插入位置的停止条件就是待插入元素大于当前比较元素。这一点在代码可以很明显体现出来,下面我给出插入排序的代码:

void insert_Sort(int arr[], int length)      //代价          次数
{
	for (int i = 1; i < length; i++)       //  c1             n
	{                                     
		int val = arr[i];                //    c2            n-1   
		int j = i - 1;                  //     c4            n-1
		while (j >= 0 && val < arr[j])  //     c5            ∑(j=2~n)t(j)
		{
			arr[j + 1] = arr[j];        //     c6            ∑(j=2~n)t(j-1)
			j--;                        //     c7            ∑(j=2~n)t(j-1)
		}
		arr[j + 1] = val;              //      c8            n-1
	}
}

简单解释代码:1.外循环从1开始是因为默认将第一个元素就当做有序序列中的第一个元素,然后从第二个元素开始往里插入。

2.arr[j+1]=val就是我前面所说的 因为终止条件是待插入元素大于当前比较元素,所以由于前面执行了j--才结束了循环,我们只要在j+1的位置插入元素就行了,当然如果循环是因为j<0而结束的,那我们的j+1其实就相当于在下标0也就是数组第一个位置插入。

接下来我们分析以下插入排序的效率:

   首先还是从它的空间复杂度开始,由于插入排序没有借用任何额外的空间,所以它的空间复杂度是O(1),所以说它是一个原地排序。然后是它的稳定性,从代码中可以看出,当待插入元素等于当前比较元素时,并不会发生那个挪的操作,这也就是说相同值元素的相对位置是不会改变的。然后是它的时间复杂度,插入排序的时间复杂度最好是O(n),最坏是O(n²),平均而言是O(n²),接下来我会给出插入排序的分析方法,明白原理后自然就知道这个最好最坏究竟出现在什么时候了。

    首先外层循环判断n次(加上最后那次循环结束的判断),执行n-1次,所以只要计算循环每一步的代价就可以计算出时间复杂度了,我们可以看出每一步循环最大的时间代价就是那个while内层循环,如果我们可以度量出这个内层循环的代价,那么整个排序的时间复杂度就出来了。我之前说过 最优的时候是O(n)那么什么时候是最优呢?答案是已排好序的情况!此时内部循环由于一开始就违反了循环条件所以内层的while循环相当于O(1)的代价,那么我们自然很轻易就算出此时插入排序时间复杂度是O(n)。那么什么时候是最坏情况呢?答案是逆序!为什么呢 我们来看看 当序列是逆序时,假设我们待插入元素的位置是j,则我们要经过j-1次挪的操作才能真正找到最后的位置。比如 下标2的元素 需要挪一次,下标3的元素需要挪2次,一直推导会发现这是一个等差数列,等差数列前n项和忽略常系数的话最后是n平方阶。那么有人会问为什么平均时间复杂度为什么也是O(n²)呢? 这是因为我们分析算法时,最坏时间复杂度往往是一个上界,我们一般分析算法效率时会针对每种情况的出现概率乘以各自消耗的时间以求期望。针对插入排序针对每一种情况的出现概率,以及各自消耗的时间相加同样是n平方级数。

  最后我会补充几个关于插入排序的面试考点:

   1.插入排序的效率 最好和最坏分别是在什么时候?(前面已经提过)

   2.链表可以插入排序么?(可以)

  答:偷个懒 就写个思路吧!^_^  有两种方式 一种是像之前一样 一个一个线性查找 值不断往后挪 如果拷贝操作的效率不高的话,我们可以利用链表的删除高效的特性。先将待插入元素从链表脱离出来,然后找到待插入的位置,将其插入进去,这样就可以省去了挪的操作,提高效率,因为链表的插入删除可以在O(1)完成,所以借用链表的特殊物理组织形式,我们省去了元素一个个向后挪的操作。

   3.针对插入排序 你是否可以进一步优化?(答案就是第四点提到的二分查找) 

 答:采用一种特殊的二分查找(找到大于等于val的第一个位置)代替线性查找。(注:此时用的是一种特殊的二分,二分查找有四种形式 之后会单独开一记讲讲)

int binary_find_1(int arr[], int length,int value)
{
	int low = 0, high = length - 1;
	while (low <= high)
	{
		int mid = low + ((high - low) >> 1);
		if (arr[mid] >= value)
		{
			if (mid == 0 || arr[mid - 1] < value)
				return mid;
			else
				high = mid - 1;
		}else {
			low = mid + 1;
		}
	}
}
void insert_Sort_1(int arr[], int length)
{
	for (int i = 1; i < length; i++)
	{
		int val = arr[i];
		int j = i - 1;
		int insert_pos=binary_find_1(arr, i, val);
		while (j>=insert_pos)
		{
			arr[j + 1] = arr[j];
			j--;
		}
		arr[insert_pos] = val;
	}
}

   4.同接上一题 我们知道二分查找的效率是O(log n)那么如果我们内层循环采用二分查找的话,效率是否可以达到O(nlogn)?

   答:我们来解释一下第四题,首先我们使用二分查找代替前面的线性查找,提升了查找的效率,但是为什么最终的时间复杂度依然是O(n²)呢?原因在于我们忽略了插入排序的一个关键操作,“向后挪” 无论我们使用线性查找还是二分查找都是为了找到最终待插入的位置。而找到待插入的位置我们依然还需要将待插入位置一直到已排序序列尾 向后挪一个位置以给待插入元素腾位置。挪的步数是不会因为你采用的查找算法而有所不同。线性查找是一边查找一边挪,二分查找是找到后一起往后挪。

猜你喜欢

转载自blog.csdn.net/qq_31984717/article/details/84134315
今日推荐