算法第七记-快速排序

今天来记录一下快速排序的思路,效率分析,以及相关的面试算法题:

快速排序与前面讲过的归并排序有着一部分相同的思想,基于分治+ partion操作。这个组成和归并排序很像,我们之前讲过归并=分治+合并。所以讲快排时我们同样先讲这个partion操作:

基本思路:假设我们取序列第一个元素为划分基准,我们定义一个边界控制边界左边的值都是小于这个基准元素,右边都是大于这个基准元素然后我们定义一个指针从头开始遍历这个序列,如果遇到小于基准元素的就与左边界+1这个位置进行交换,同时左边界也向右移动了一格。直到遍历到序列尾部才结束。

比如序列开始时是:5,3,4,6,7,1,0,2,2第一趟划分之后将以5为边界2,3,4,1,0,2,5,6,7

int partion(int arr[],int low, int high)
{
	int val = arr[low];
	int left = low - 1;
	for(int i=low;i<=high;i++)
	  if (arr[i] <= val)
	  {
			left++;
			swap(arr[left], arr[i]);
		}
	swap(arr[low], arr[left]);
	return left;
}

另外还有一种partion的写法:这种写法的思路大体一样,它是分别从两边一起向中间靠定义两个边界指针向左,向右开始我们将第一个元素定为基准元素,然后如果右边界的元素是大于基准元素的话,则右边界向左移动一个位置,如果小的话与当前的左边界进行交换。通俗来讲,我们假设每个元素都由一个坑装着,我们先把第一个坑里的元素拿走,此时我们有一个坑是空的,所以我们需要从其他地方找东西来填补,这个填补的东西就是右边那些比基准元素小的值。每当我们从右边拿走一个值去补左边时,右边又会多出一个坑来,就这样不断重复。直到左边界与右边界相遇。

int partion(int arr[],int low,int high)
{
	int val = arr[low];
	int left = low;
	int right = high;
	while (left <right)
	{
		while (left < high&&arr[right] >= val)
			right--;
		if (left < right)
			arr[left] = arr[right];
		while (left < right&&arr[left] <=val)
			left++;
		if (left < right)
			arr[right]=arr[left];
	}
	arr[left] = val;
	return left;
}

那么这一步partion的作用是什么呢?每一次partion我们都可以使得一个元素到达最后排序完成的最终位置。

接下来我们讲讲分治做了什么:

    每一次划分子序列,我们都是根据之前得到的partion返回值,继续划分子序列。所以由于partion的位置不能确定,所以划分的子序列大小并不一定相同,这与归并排序有所不同,归并排序每次都是从中间划分,所以归并排序划分子序列的时候两个子序列各占原序列一半。

  

void QuickSort_core(int arr[], int low, int high)
{
	if (high - low > 0)
	{
		int pos = partion(arr, low, high);
		QuickSort_core(arr,low, pos - 1);
		QuickSort_core(arr, pos + 1, high);
	}
}
void QuickSort(int arr[], int length)
{
	if (!arr || length <= 0)
		return;
	QuickSort_core(arr, 0, length - 1);
}

下面我们来分析一下快速排序的效率:快速排序由于没有使用额外的空间,所以它的空间复杂度是O(1),由于快速排序会进行左右交换,自然就不能保证稳定性比如3 0 1 2 2 4以3进行划分2 0 1 2 3 4靠后面的那个2换到了前面,使得它们的相对位置发生了改变。关于时间复杂度的分析,我们可以参考之前归并排序的递归树分析法,每一次划分都会有两个子序列,归并是对半分的,而由于partion得到的位置时不确定的,那么从这个方向出发,我们可以思考一下快速排序的最好最坏情况分别在什么时候。

对于递归树的每一层都需要消耗为O(n)的时间,假设递归树的高度为ħ的话,那么快速排序的时间复杂度就是O(H * n)时,那么所有的问题就落在了这个ħ的确定上了。最好情况下,得到的递归树是一棵完全二叉树,它的高度接近O(logn)时间时间。那么最好的时候就是O(nlogn),那么最坏情况呢?我们会想到高度最高的时候,树在什么时候高度会最高?左斜树或者右斜树。那么递归树在什么情况下会变成左斜树或者右斜树?答案是有序!因为我们每次都是拿序列中第一个元素作为基准,比如第一次划分的时候我们就只能划分为两个子序列,一个子序列只有一个元素,另一个子序列有n-1个个元素。以此类推我们不难发现树的高度变成了为O(N),那么最终的时间复杂度就上升到了O(N²)。那么平均而言如果我们划分的序列大小之间的比例不随ñ变化,是一 O(nlogn)。通常来说,我们选取​​基准元素的方式有很多种,选第一个,最后一个,随机选。而我们一般考虑算法的随机性,我们会采用随机选择基准元素。

下面我们总结一些利用到了快排思路的算法题:

1.给定一个序列,编写算法使得偶数在奇数前面

答:这个题用到了我们之前讲的partion的思路,定义边界控制,定义一个新的指针遍历如果遇到偶数与边界右边的位置的值进行交换,然后边界向右移动一个位置。

void partion_odd_even(int arr[], int length)
{
	if (!arr || length <= 0)
		return;
	int left = -1;
	int p = 0;
	while (p < length)
	{
		if (arr[p] % 2== 0)
		{
			left++;
			swap(arr[left], arr[p]);
		}
		p++;
	}

}

2.有一个只由0,1,2三种元素构成的整数数组,请使用交换,原地排序而不是使用计数进行排序。

给定一个只含0,1,2整数的数组及它的大小,请返回排序后的数组。保证数组大小小于等于500。

测试样例:

[0,1,1,0,2,2],6
返回:[0,0,1,1,2,2]

答:这其实就是一个“荷兰国旗问题”,与上题思路类似,不过它要控制两个边界,一个左边界控制0,一个右边界控制2,然后一个指针从头开始遍历,遍历到0与左边界1的位置的值交换,然后左边界1,遍历到2就与右边界-1的位置的值进行交换,然后右边界-1。此时由于从右边换过来的元素我们并不知道它是0,或是1或是2所以此时负责遍历的指针不能向前移动。

void partion_0_1_2(int arr[], int length)
{
	int left = -1;
	int right = length;
	int p = 0;
	for(int p=0;p<right;p++)
	{
		if(arr[p]==1)
			continue;
		if (arr[p] == 0)
		{
			left++;
			swap(arr[left], arr[p]);
		}
		if (arr[p] == 2)
		{
			right--;
			swap(arr[right], arr[p]);
			p--;
		}
	}
}

3.求数组第我大或者第我小的数(思路类似)

答:。利用我们之前用到过的partion方法,我们可以将序列分为两个子序列因此根据这个思路我们可以不断调用partion直到当前划分的位置为我们所指定的我这里我们为了使得partion这个算法更具随机性普遍性。我们选取基准元素采用范围随机数选取。

与快速排序一样,我们依然把序列进行递归划分。有所不同的是,快排会递归处理两边。而这个算法只需要处理一边就行。最差情况就是遍历整个序列为O(n),例如一个有序序列我们的运气很倒霉使用了随机采取基准元素依然每次都取到了左边缘的元素,假设我们要找第n大的数字,那么找的过程就成了之前我们快排类似的最坏情况了。最坏是O(N²),但是由于我们采取的是随机采取基准元素,所以遇到最坏情况的概率很低,所以平均来说时间复杂度是O(N)。

int partion(int arr[],int low,int high)
{
	int val = arr[low];
	int left = low;
	int right = high;
	while (left <right)
	{
		while (left < high&&arr[right] >= val)
			right--;
		if (left < right)
			arr[left] = arr[right];
		while (left < right&&arr[left] <=val)
			left++;
		if (left < right)
			arr[right]=arr[left];
	}
	arr[left] = val;
	return left;
}
int random_partion(int arr[], int low, int high)
{
	int i = random(low, high);
	swap(arr[i], arr[low]);
	return partion(arr, low, high);
}
int partion_mid_select(int arr[],int low,int high,int i)
{
	if (high == low)
		return arr[low];
	int pos=random_partion(arr, low, high);
	int k = pos - low + 1;//如果我们的序列长度为9我们的i可能是[1,9],而数组坐标下标是[0,8],
//例如下标6这个位置 其实是第七个元素。所以如果我们得到的pos实际上是i-1。然而我们要判断这个pos这个位置是否是第i个元素,需要加1,并与我们实际的i进行比较,比较符合的话,返回的应该是原来的pos
//3 2 0 6 3 4  找第1个元素 当0位于第一个位置时 实际上下标为0,而我们要找的是第一个元素 所以需要pos-low+1(此时等于1)与i进行比较 相等就返回arr[0]
	if (k == i)
		return arr[pos];
	else if (i < k)
		return partion_mid_select(arr, low, pos - 1, i);
	else
		return partion_mid_select(arr, pos + 1, high, i - k);
}

4.找出数组中出现次数超过一半的数字

答:。当一个数在数组里出现次数超过一半的时候,那它本身也就是中位数也就是与第五题类似这里我贴上剑指报价上的解法

需要注意的是我们需要注意非法的输入,如果频率最高的元素都没有达到一半的标准。这就是后面函数的作用。

int MoreThanHalfNum(int arr[],int length)
{
   if(CheckInvailedArray(arr,length))
      return 0;
   int mid=length/2;
   int start=0;
   int end=length-1;
   int index=partion(arr,strat,end);
   while(index!=mid)
   {
      if(index<mid)
       {
         start=index+1;
         index=partion(arr,start,end);
      }
      else{
           end=index-1;
         index=partion(arr,low,end);
       }  
 }
     int result=arr[mid];
    if(!CheckMoreThanHalf(arr,length,result))
      return 0;
   return result;
}
bool input_vaild=false;
bool CheckInvaildArray(int arr[],int length)
{
    input_vaild=false;
    if(!arr||length<=0)
      return input_vaild;
    input_vaild=true;
}
bool CheckMoreThanHalf(int arr[],int length,int number)
{
    int times=0;
   for(int i=0;i<length;i++)
     if(arr[i]==number)
        times++;
  bool is_MoreThanHalf=true;
  if(2*times<length)
   {
      is_MoreThanHalf=false;
      input_vaild=true;
   }
   return is_MoreThanHalf;
}

5.求数据流中的中位数

答:求数据流中的中位数的思路是,如果序列大小为奇数时,排序后的序列排在中间的数字就是中位数,如果为偶数的话,则在中间的两个数的平均值是中位数。所以我们先判断序列的大小,然后我们可以使用之前的快排partion思路找到第ķ小的数字的值,如果是大小为奇数只要找到第n / 2个小的数就行,如果是偶数找到第n / 2个小,第N / 2 + 1小的两个数求平均值就行。

6.链表的快速排序

答:单链表由于它的物理存储性质使得它不能使用我们上面提到过的从两边向中间靠的partion,但是可以使用第一种只控制一个边界,然后不断遍历,如果是比基准元素小的则与边界交换,边界向右移动一个位置。唯一的不同就是链表的判断结束条件与数组是不同的,这点需要注意。

list_node* get_position(list_node* beg, list_node* end)
{
	list_node* p = beg;
	list_node* q = p->next;
	int val = beg->num;
	while (q!=end)
	{
		if (q->num < val)
		{
			swap(p->next->num, q->num);//
			p = p->next;
		}
		q = q->next;
	}
	swap(p->num, beg->num);
	return p;
}
void quick_sort(list_node* beg,list_node* end)
{
	if (beg != end)
	{
		list_node* pos = get_position(beg, end);
		quick_sort(beg, pos);
		quick_sort(pos->next, end);
	}
}

猜你喜欢

转载自blog.csdn.net/qq_31984717/article/details/84323676