数据结构复习笔记 - 排序(下)

归并排序和快速排序

  • 时间复杂度为 O(nlogn)

归并排序

归并排序使用的就是分治思想。分治,顾名思义,就是分而治之,将一个大问题分解成小的子问题来解决。小的子问题解决了,大问题也就解决了。

分治算法一般都是用递归来实现的。分治是一种解决问题的处理思想,递归是一种编程技巧,这两者并不冲突。


递推公式:
merge_sort(p…r) = merge(merge_sort(p…q), merge_sort(q+1…r))

终止条件:
p >= r 不用再继续分解

merge_sort(p…r) 表示,给下标从 p 到 r 之间的数组排序。我们将这个排序问题转化为了两个子问题,merge_sort(p…q) 和 merge_sort(q+1…r),其中下标 q 等于 p 和 r 的中间位置,也就是 (p+r)/2。当下标从 p 到 q 和从 q+1 到 r 这两个子数组都排好序之后,我们再将两个有序的子数组合并在一起,这样下标从 p 到 r 之间的数据就也排好序了。

伪代码


// 归并排序算法, A是数组,n表示数组大小
merge_sort(A, n) {
  merge_sort_c(A, 0, n-1)
}

// 递归调用函数
merge_sort_c(A, p, r) {
  // 递归终止条件
  if p >= r  then return

  // 取p到r之间的中间位置q
  q = (p+r) / 2
  // 分治递归
  merge_sort_c(A, p, q)
  merge_sort_c(A, q+1, r)
  // 将A[p...q]和A[q+1...r]合并为A[p...r]
  merge(A[p...r], A[p...q], A[q+1...r])
}

merge(A[p…r], A[p…q], A[q+1…r]) 这个函数的作用就是,将已经有序的 A[p…q] 和 A[q+1…r] 合并成一个有序的数组,并且放入 A[p…r]。那这个过程具体该如何做呢?

我们申请一个临时数组 tmp,大小与 A[p…r] 相同。我们用两个游标 i 和 j,分别指向 A[p…q] 和 A[q+1…r] 的第一个元素。比较这两个元素 A[i] 和 A[j],如果 A[i]<=A[j],我们就把 A[i] 放入到临时数组 tmp,并且 i 后移一位,否则将 A[j] 放入到数组 tmp,j 后移一位。

继续上述比较过程,直到其中一个子数组中的所有数据都放入临时数组中,再把另一个数组中的数据依次加入到临时数组的末尾,这个时候,临时数组中存储的就是两个子数组合并之后的结果了。最后再把临时数组 tmp 中的数据拷贝到原数组 A[p…r] 中。

我们把 merge() 函数写成伪代码,就是下面这样:


merge(A[p...r], A[p...q], A[q+1...r]) {
  var i := p,j := q+1,k := 0 // 初始化变量i, j, k
  var tmp := new array[0...r-p] // 申请一个大小跟A[p...r]一样的临时数组
  while i<=q AND j<=r do {
    if A[i] <= A[j] {
      tmp[k++] = A[i++] // i++等于i:=i+1
    } else {
      tmp[k++] = A[j++]
    }
  }
  
  // 判断哪个子数组中有剩余的数据
  var start := i,end := q
  if j<=r then start := j, end:=r
  
  // 将剩余的数据拷贝到临时数组tmp
  while start <= end do {
    tmp[k++] = A[start++]
  }
  
  // 将tmp中的数组拷贝回A[p...r]
  for i:=0 to r-p do {
    A[p+i] = tmp[i]
  }
}

快速排序

1.快速排序算法是什么?

想必大家都学过和用过冒泡排序吧!这应该是大家成为程序员道路上学的第一个算法哦,那么我们的快速排序算法其实是在冒泡排序的基础上的一个改进,快速排序算法是利用一趟快速排序,一趟快排一般都是取第一个数作为准基数进行排序,将一串数据分成两个部分,

第一部分都比准基数小,第二部分都比准基数大,如:(-------第一部分------准基数------第二部分),也就这样以准基数分成了两个部分,接下来这两个部分继续使用一趟快排(可以用递归的方法),以此类推,最后数据显示是从小到大的排列顺序。

2.一趟快速排序是什么?

我们我们先建立一组数据:

12 30 5 16 5 1 20 数据
0 1 2 3 4 5 6 下标
  1. 根据一趟快排的规则,我们先定下一个准基数,通常准基数都是数据的第一位也就是这里的数字12
  1. 然后一趟快排是先从下标6向左进行和准基数(12)的比较,比较完一个后,然后再从下标0向右进行和准基数(12)的比较。
  1. 我们进行比较的规则和操作是:从下标6向左进行和准基数(12)的比较,只要遇到的数据小于准基数(12)的时候我们就将这个数和准基数(12)进行替换,然后再执行从下标0向右进行和准基数(12)的比较

如:我们从下标6向左进行和准基数(12)的比较,20>12,不满足一趟快排的规则,寻找下一位,1<12,所以我们将下标0的值和下标5的值进行替换替换后的结果为:

1 30 5 16 5 12 20 数据
0 1 2 3 4 5 6 下标
  1. 执行完上一步之后,我们再从下标0向右进行和准基数(12)的比较,这里的规则是只要遇到的数据大于准基数(12)的时候我们就将这个数和准基数(12)进行替换,和上面一步恰恰相反

如:我们再从下标0向右进行和准基数(12)的比较,30>12,所以我们将下标1的值和下标5的值进行替换

1 12 5 16 5 30 20 数据
0 1 2 3 4 5 6 下标
  1. 从左到右查找和从右到左的查找,都是通过下标来查找的,只要它们两者下标不相遇就可以一直进行排序,直到相遇后第一次一趟快排结束,不过,总有一次会相遇的。好啦,执行完上一步之后,基本的套路也就生成了,你们可以试一下继续执行3,4步骤吧!

  2. 第一次一趟快排结束显示结果为:

1 5 5 12 16 30 20 数据
0 1 2 3 4 5 6 下标
  1. 从上面第一次一趟快排结果我们可以看出从准基数那里将数据分成的两个部分,前面那一部分,1,5,5,都比准基数要小,后面的16,30,20,则比准基数要大。但是这还不算完,我们明显的看到排序并非从小到大。所以说我们需要把这整一条数据分成1,5,5和16,30,20这两个条数据再次进行一趟快排(递归),以此类推,直到排出一条规矩的数据为止

最后结果为:

1 5 5 12 16 20 30 数据
0 1 2 3 4 5 6 下标

OC代码实现

- (void)viewDidLoad {
    [super viewDidLoad];

    NSMutableArray *arr = [[NSMutableArray alloc] initWithObjects:@(6), @(1),@(2),@(5),@(9),@(4),@(3),@(7),nil];
    
    [self quickSortArray:arr withLeftIndex:0 andRightIndex:arr.count - 1];
    
    NSLog(@"%@",arr);
    
}

- (void)quickSortArray:(NSMutableArray *)array withLeftIndex:(NSInteger)leftIndex andRightIndex:(NSInteger)rightIndex
{
    if (leftIndex >= rightIndex) {//如果数组长度为0或1时返回
        return ;
    }
    
    NSInteger i = leftIndex;
    NSInteger j = rightIndex;
    //记录比较基准数
    NSInteger key = [array[i] integerValue];
    
    while (i < j) {
        /**** 首先从右边j开始查找比基准数小的值 ***/
        while (i < j && [array[j] integerValue] >= key) {//如果比基准数大,继续查找
            j--;
        }
        //如果比基准数小,则将查找到的小值调换到i的位置
        array[i] = array[j];
        
        /**** 当在右边查找到一个比基准数小的值时,就从i开始往后找比基准数大的值 ***/
        while (i < j && [array[i] integerValue] <= key) {//如果比基准数小,继续查找
            i++;
        }
        //如果比基准数大,则将查找到的大值调换到j的位置
        array[j] = array[i];
        
    }
    
    //将基准数放到正确位置
    array[i] = @(key);
    
    /**** 递归排序 ***/
    //排序基准数左边的
    [self quickSortArray:array withLeftIndex:leftIndex andRightIndex:i - 1];
    //排序基准数右边的
    [self quickSortArray:array withLeftIndex:i + 1 andRightIndex:rightIndex];
}

内容小结

归并排序和快速排序是两种稍微复杂的排序算法,它们用的都是分治的思想,代码都通过递归来实现,过程非常相似。理解归并排序的重点是理解递推公式和 merge() 合并函数。同理,理解快排的重点也是理解递推公式,还有 partition() 分区函数。

归并排序算法是一种在任何情况下时间复杂度都比较稳定的排序算法,这也使它存在致命的缺点,即归并排序不是原地排序算法,空间复杂度比较高,是 O(n)。正因为此,它也没有快排应用广泛。

快速排序算法虽然最坏情况下的时间复杂度是 O(n2),但是平均情况下时间复杂度都是 O(nlogn)。不仅如此,快速排序算法时间复杂度退化到 O(n2) 的概率非常小,我们可以通过合理地选择 pivot 来避免这种情况。

你可能感兴趣的:(数据结构复习笔记 - 排序(下))