数据结构和算法之美_12_归并排序、快速排序

第二次整理快速排序。理解了怎么使用分治和递归实现快速排序,在分区的过程中需要选定pivot,并巧妙利用双指针将数据重排,返回分区点的下标用于递归调用。时间复杂度的推导尽力理解了,它的时间复杂度与每次分区后遍历元素次数相关。值得注意的是递归调用的时间复杂度也是通过递归公式推导得到的。以上花费2小时。

再次巩固一下,这简直是灾难性遗忘。利用pivot进行分区,需要指针j遍历每一个元素与pivot进行比较,满足条件a[j]小于pivot则交换指针i和j指向的元素,同时i++。

归并排序的递归思想比较好理解,但是需要注意分区递归调用完需要合并到一起,即将两个有序区间重新排序。使用的方法比较常规,请帮我牢牢焊在脑子里。以上花费1个小时。

归并排序(merge sort)

一、什么是归并排序?
使用归并排序对一组数据排序,首先将数据分为两个部分,再对两部分数据重新排序,最后将排序后的两部分数据合并成一组数据并使它有序。
在这里插入图片描述
归并排序算法采用了分治思想,分治就是将一个大问题分解成各个小的子问题来解决,和用递归解决问题的思路很像,事实上使用分治思想解决问题也需要用到递归的编程技巧。

//p是数组区间的最左边元素的下标,r是最右边元素的下标,q是分区点下标。
void * merge(int a[], int p, int q, int r)
{
	int *tmp;
	int i, j, k;
	tmp = (int *)malloc(sizeof(int)*(r - p + 1));
	if (!tmp)
	{
		abort();
	}
	for (i = p, j = q + 1, k = 0; i <= q && j <= r ;)
	{
		if (a[i] <= a[j])
		{
			tmp[k++] = a[i++];
		}
		else
		{
			tmp[k++] = a[j++];
		}
	}
	if (i = q + 1)//处理前半部分全部已排序的情况
	{
		for (; j <= r;)
		{
			tmp[k++] = a[j++];
		}
	
	}
	else//处理后半部分全部已排序情况
	{
		for (; i <= q;)
		{
			tmp[k++] = a[i++];
		}
	}
	memcpy(a, tmp, sizeof(int)*(r - p + 1));
	free(tmp);
}

//下面函数体现了归并排序的分解和合并思想
//考虑测试用例:p=0 , r=1;p=0 , r=0代码适用
void merge_sort(int a[], int p, int r)
{
	if (p >= r)
		return;
	int q = (p + r) / 2;
	merge_sort(a, p, q);
	merge_sort(a, q + 1, r);
	merge(a, p , q , r);
}

leetcode 921.排序算法 归并实现

链接

class Solution {
    //归并排序:通过递归和归并实现
    //首先通过递归将数组分成前后两个区间
    //递归结束时对两个区间进行排序,并将排序后的两个区间合并成一个区间,并返回上一次递归调用
public:
    void merge(vector<int>& nums, int l, int r, int mid) {
        //malloc一个数组用于临时存放sort的结果
        int *tmp = (int *)malloc((r-l+1) * sizeof(int));
        int k = 0, i = l, j = mid + 1;

        for( ; i <= mid && j <= r;  )
        {
            //稳定的
            if(nums[i] > nums[j]){
                tmp[k++] = nums[j++];
            }
            else
            {
                tmp[k++] = nums[i++];
            }

        }

        while(i <= mid){
            tmp[k++] = nums[i++];
        }     

        while(j <= r) {
            tmp[k++] = nums[j++];
        }

        memcpy(&nums[l], tmp, (r - l + 1) * sizeof(int));

        free(tmp);
    }

    void merge_sort(vector<int>& nums,  int l, int r)
    {
        if(l >= r)
        {
            return;
        }
        int p = (r - l) / 2 + l;
        merge_sort(nums, l, p);
        merge_sort(nums, p+1 , r);
        merge(nums, l, r, p);
    }

    vector<int> sortArray(vector<int>& nums) {
         merge_sort(nums, 0, nums.size()-1);
        return nums;
    }
};

归并排序的其他特性

1、归并排序是一种稳定的算法:因为将一组数据分成前后两个部分,若比较的两个数据相等,我们可以让前面部分的数据先添加到合并后的数组中,从而保证了归并排序的稳定性。
2、归并排序的执行效率与要排序的原始数组的有序程度无关,所以其时间复杂度是非常稳定的,不管是最好情况、最坏情况,还是平均情况,时间复杂度都是 O(nlogn)。
3、归并排序不是原地排序算法:在任意时刻,CPU 只会有一个函数在执行,也就只会有一个临时的内存空间在使用。临时内存空间最大也不会超过 n 个数据的大小,所以空间复杂度是 O(n)。

快速排序

任意选取一个数据作为分区点(pivot),通常选数组的最后一个元素。我们遍历 p 到 r 之间的数据,将小于 pivot 的放到左边,将大于 pivot 的放到右边,将 pivot 放到中间。经过这一步骤之后,数组 p 到 r 之间的数据就被分成了三个部分,前面 p 到 q-1 之间都是小于 pivot 的,中间是 pivot,后面的 q+1 到 r 之间是大于 pivot 的。我们用一个分区函数partition() 来实现这一过程。
实现分区函数:
1、不考虑空间消耗:可以申请两个数组,然后遍历这组数据,将小于分区点的放到一个临时数组中,大于分区点的数据放到另一个临时数组中。然后将临时数组中的数据顺序拷贝到原来的数组中。
2、考虑空间消耗,原地排序实现:
伪代码:

partition(A, p, r) {
  pivot := A[r]
  //巧妙的利用双指针将小于pivot数据放到右边,将大于pivot的数据放到左边
  //一次遍历后将pivot点放到正确的位置
  i := p
  for j := p to r-1 do {
    if A[j] < pivot {
      swap A[i] with A[j]
      i := i+1
    }
  }
  swap A[i] with A[r]
  return i

示意图如下:
在这里插入图片描述

理解:
1、为了将小于分区点的数据放到分区点的右边,我们用两个指针,指针i用来指向可以用来交换的位置,所以只有当交换发生时,指针i才指向下一个用来待交换的位置。
2、我们需要用指针j遍历数组依次指向数组中的每一个元素,每个元素可能小于分区点也可能大于分区点,当j当前指向的元素大于分区点的时候不发生交换,指针j指向下一个位置的元素对其进行处理;当指针j当前指向的元素小于分区点的元素的时候需要交换元素,目的是将小于分区点的元素放到数组的右侧,我们利用指针i指向的位置,给我们提供交换的位置,依次需要将指针i和指针j指向的内容交换。
3、当指针j遍历到最后一个元素(分区点)时,所有小于8的元素都放在的数组的最右侧,这时指针i指向的位置就是排序后分区点元素应该所在的位置。

下面是c++代码实现:

//元素交换
void swap(int *a, int *b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}

//分区函数,将数组分为小于分区点、分区点、大于分区点三部分
//返回值:返回分区点元素的下标
int partition(int a[], int p, int r)
{
	int i = p;
	int pivot = a[r];
	//这个for循环帮我牢牢的焊在脑子里,双指针,j遍历的每一个元素都要与pivot比较大小
	//小于pivot,i指针与j指针指向的元素交换,i指针挪向下一个待交换的坑位
	for (int j = p;; j <= r-1; j++)
	{
		if (a[j] < pivot)
		{
			swap(a+i, a+j);
			++i;
		}
	}
	swap(a + i, a + r);
	return i;
}

//用递归技巧实现快速排序
void _quicksort(int a[], int p, int r)
{
	if (p >= r)
	{
		return;
	}
	int q = partition(a , p , r);
	_quicksort(a, p, q - 1);
	_quicksort(a, q + 1, r);
}

void quick_sort(int a[], int size)
{
	_quicksort(a, 0, size - 1);
}

leetcode 921.排序数组

class Solution {
    //快速排序
    //随机数种子发生器
    //srand((unsigned)time(NULL));
    //随机分区点下标计算方法
    //int pivoit_idx =  rand() % (l - r + 1) + r
public:
    int partition(vector<int> &nums, int r, int p)
    {
        int i = r;
        for(int j = r; j <= p - 1; j++){
            if(nums[j] <= nums[p]){
                swap(nums[i], nums[j]);
                i++;
            }
        }
        swap (nums[i], nums[p]);
        return i;
    }

    int randomized_partition(vector<int> & nums, int r, int p ){
        int i = rand() %  (p - r + 1) + r; //随机选一个作为我们的主元
        swap(nums[p], nums[i]);
        return partition(nums, r, p);

    }

    void quick_sort(vector<int> &nums, int r, int p)
    {
        if(r >= p)
        {
            return;
        }

        int q = randomized_partition(nums, r, p);
        quick_sort(nums, r, q-1);
        quick_sort(nums, q+1, p);
    }
    vector<int> sortArray(vector<int>& nums) {
        srand((unsigned)time(NULL));
        quick_sort(nums, 0, nums.size() - 1);
        return nums;
    }
};

元素重复的大数组排序超时,需要进一步优化。

快速排序的时间复杂度分析

第一次分区查找我们需要遍历的数组长度为n。第二次分区查找,需要遍历 n/2 个元素。依次类推,分区遍历元素的个数分别为、n/2、n/4、n/8、n/16.……直到区间缩小为 1。

如果我们把每次分区遍历的元素个数加起来,就是:n+n/2+n/4+n/8+…+1。这是一个等比数列求和,最后的和等于 2n-1。所以,上述解决思路的时间复杂度就为 O(n)。

快速排序的其他特性

1、快排是一种原地的、不稳定的排序方式。原地排序的空间复杂度为O(1)。
2、它的时间复杂度最好情况O(n)、最坏情况O(n^2)、平均情况的时间复杂度都是O(nlogn)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值