第12章 排序与选择 的基本知识及python实现

Date:2019-07-13

目录:

12.1 为什么要学习排序算法

12.2 归并排序(12.2.1 分治法 12.2.2 基于数组的归并排序的实现 12.2.3 归并排序的运行时间 12.2.5 归并排序的可选实现)

12.3 快速排序 (12.3.1 随机快速排序 12.3.2 快速排序的额外优化 )

12.4 再论排序:算法视角(12.4.1 排序下界 12.4.2 线性时间排序:桶排序和基数排序)

12.5 排序算法的比较

12.6 python的内置排序函数 

12.7 选择(12.7.1 剪枝搜索 12.7.2 随机快速选择 12.7.3 随机快速选择分析)

 

12.1 为什么要学习排序算法

本章的重点是针对对象进行排序的算法。我们主要针对一个集合的元素进行重新排列,以使我们按照从小到大的顺序进行排列,当然我们假设的前提是存在这样的一个排序的。

python对数据排序提供了内置函数支持,其中包括重新对列表内容进行排序的list类的sort方法,还有以排好的顺序生成一个包含任意元素集合的内置的sorted函数。这些内置函数使用了一些高级算法,并且是高度优化的。由于很少有需要从头开始实现排序算法的特殊情况出现,因此用户一般使用python内置的排序函数。但对于初学者,对排序算法进行详细的了解还是非常有必要的。

本章主要重点讲解了归并算法(分治思想、基于数组的归并递归算法的实现、基于队列的归并递归算法的实现、基于list的非递归归并算法的实现)、快速排序(基于队列的快速排序算法的实现、随机快速排序、基于list的就地快速排序算法的实现)、通排序、基数排序、几种排序算法的比较、python的内置排序算法和选择算法的介绍和实现。

12.2 归并排序

12.2.1 分治法

分而治之思想的实现载体是递归算法的应用。分治法设计模式包含以下三个步骤:

1) 分解:如果输入值的规格小于确定的阈值,我们就通过使用直截了当的方法来解决这些问题并返回获得的答案。否则,我们把输入值分解为两个或者更多的互斥子集。

2) 解决子问题:递归地解决这些与子集相关的子问题

3) 合并:整理这些子问题的解,然后把它们合并成一个整体用以解决最开始的问题。

使用分治法进行排序,有以下三个步骤来对一个有n个元素的序列S进行排序,归并排序过程如下:

1) 分解: 若S只有0个或1个元素,直接返回S(因为已经排好序了);否则从S中移除所有元素,然后将他们放在S1,S2两个序列中,每一个序列包含S中一半的元素,也就是说,S1包含S前一半的元素,S2包含S后一半的元素。

2) 解决子问题:递归地对S1和S2进行排序。

3)合并:把这些分别在S1和S2中排好序的元素拿出来按照顺序合并到S序列中。

可以用一个二叉树T来形象化一个归并排序算法的执行过程,称这个二叉树为归并排序树。由于输入序列的大小在每个递归调用中减半,因此归并排序树的高度大约是logn。

命题:在大小为n的序列上执行归并算法,与其关联的归并排序树的高度为 \left \lceil log n \right \rceil.

12.2.2 基于数组的归并排序的实现

先定义一个merge函数实现一个子任务:负责将之前提到的两个已经排好序的序列S1和S2合并,并将输出复制到序列S中。

然后再是merge_sort函数实现基于数组的递归的归并排序算法(合并是较为复杂的,需要按照顺序进行排序)

# python中基于数组list类的合并操作的执行过程
def merge(S1, S2, S):
  """Merge two sorted Python lists S1 and S2 into properly sized list S."""
  i = j = 0
  while i + j < len(S):
    if j == len(S2) or (i < len(S1) and S1[i] < S2[j]):
      S[i+j] = S1[i]      # copy ith element of S1 as next item of S
      i += 1
    else:
      S[i+j] = S2[j]      # copy jth element of S2 as next item of S
      j += 1
# python中基于数组的list类的递归归并排序算法的执行过程
def merge_sort(S):
  """Sort the elements of Python list S using the merge-sort algorithm."""
  n = len(S)
  if n < 2:
    return                # list is already sorted
  # divide
  mid = n // 2
  S1 = S[0:mid]           # copy of first half
  S2 = S[mid:n]           # copy of second half
  # conquer (with recursion)
  merge_sort(S1)          # sort copy of first half
  merge_sort(S2)          # sort copy of second half
  # merge results
  merge(S1, S2, S)        # merge sorted halves back into S

命题:假设一个大小为n的序列S,其两个元素可以在O(1)的时间内完成比较,那么归并排序算法对S进行排序消耗的时间为O(n*logn).

12.2.5 归并算法的可选实现

排序链表

任何一种形式的基本队列都可以很容易地作为归并排序算法的容器类型。

下面是基于基本队列的递归的归并排序算法的实现:

# 基于链表队列实现该递归归并排序算法
from ..ch07.linked_queue import LinkedQueue
# 合并操作:其相比较分解函数部分,合并函数更复杂。
def merge(S1, S2, S):
  """Merge two sorted queue instances S1 and S2 into empty queue S."""
  while not S1.is_empty() and not S2.is_empty():
    if S1.first() < S2.first():
      S.enqueue(S1.dequeue())
    else:
      S.enqueue(S2.dequeue())
  while not S1.is_empty():            # move remaining elements of S1 to S
    S.enqueue(S1.dequeue())
  while not S2.is_empty():            # move remaining elements of S2 to S
    S.enqueue(S2.dequeue())

def merge_sort(S):
  """Sort the elements of queue S using the merge-sort algorithm."""
  n = len(S)
  if n < 2:
    return                            # list is already sorted
  # divide
  S1 = LinkedQueue()                  # or any other queue implementation
  S2 = LinkedQueue()
  while len(S1) < n // 2:             # move the first n//2 elements to S1
    S1.enqueue(S.dequeue())
  while not S.is_empty():             # move the rest to S2
    S2.enqueue(S.dequeue()) # 分解结束
  # conquer (with recursion)
  merge_sort(S1)                      # sort first half
  merge_sort(S2)                      # sort second half  # 这两步实现递归调用归并算法
  # merge results
  merge(S1, S2, S)                    # merge sorted halves back into S  # 最后合并算法

自底向上的(非递归的)归并排序算法

这是一个基于数组的非递归版本的归并排序算法,运行时间是O(n*logn).在实践中,它会比递归的归并排序略快一些,因为它避免了每级的递归调用及临时内存的额外开销。这种算法的主要思想是执行自底向上的归并排序,即对整个归并排序树自底向上逐层执行合并。给出元素的一个输入数组,我们将每个连续的元素对合并成有序的,以长度为2开始执行,然后再合并至长度为4,长度为8等,依次类推……,直到整个数组已经排序完毕。

import math

def merge(src, result, start, inc):
  """Merge src[start:start+inc] and src[start+inc:start+2*inc] into result."""
  end1 = start+inc                        # boundary for run 1
  end2 = min(start+2*inc, len(src))       # boundary for run 2
  x, y, z = start, start+inc, start       # index into run 1, run 2, result
  while x < end1 and y < end2:
    if src[x] < src[y]:
      result[z] = src[x]
      x += 1
    else:
      result[z] = src[y]
      y += 1
    z += 1                                # increment z to reflect new result
  if x < end1:
    result[z:end2] = src[x:end1]          # copy remainder of run 1 to output
  elif y < end2:
    result[z:end2] = src[y:end2]          # copy remainder of run 2 to output

def merge_sort(S):
  """Sort the elements of Python list S using the merge-sort algorithm."""
  n = len(S)
  logn = math.ceil(math.log(n,2))
  src, dest = S, [None] * n               # make temporary storage for dest
  for i in (2**k for k in range(logn)):   # pass i creates all runs of length 2i
    for j in range(0, n, 2*i):            # each pass merges two length i runs
      merge(src, dest, j, i)
    src, dest = dest, src                 # reverse roles of lists
  if S is not src:
    S[0:n] = src[0:n]                     # additional copy to get results to S

12.3 快速排序

快速排序如同归并算法,同样是基于分治法的典范。但是他在使用这项技术时运用了相反的方式,即把所有复杂的操作在递归之前做完。

快速排序的高阶描述:主要思想是应用分治法把序列S分解为子序列,递归地排序每个子序列,然后通过简单的串联方式合并这些已经排好序的子序列。

1) 分解:如果S至少有2个元素(如果S只有1个或0个元素,什么都不用做),从S中选择一个特定的元素x,称之为基准值、一般情况下,选择S中最后一个元素作为基准值,从S中移除所有元素,并把它们放在以下3个序列中:

* L存储S中小于x的元素

* E存储S中等于x的元素

* G存储S中大于x的元素

当然,如果S中的元素是互异的,那么E中只含有一个元素-基准值本身而已。

2) 解决子问题:递归地排序序列L和G

3) 合并:把S中的元素按照先插入L中的元素、然后插入E中的元素、最后插入G中元素的顺序放回。

同样快速排序也可以用二叉递归树来模拟,称之为快速排序树。但是与归并排序有所区别的是,在最坏的情况下,快速排序树的高度是线性的,初始序列越接近有序,快速排序树的高度越接近序列元素的个数。在这种情况下,把最后一个元素作为基准值的标准选法会产生一个长度为n-1的子序列L、长度为1的自序列E和长度为0的自序列G。

下面是基于队列的快速排序算法的实现:

from ..ch07.linked_queue import LinkedQueue

def quick_sort(S):
  """Sort the elements of queue S using the quick-sort algorithm."""
  n = len(S)
  if n < 2:
    return                            # list is already sorted
  # divide
  p = S.first()                       # using first as arbitrary pivot
  L = LinkedQueue()
  E = LinkedQueue()
  G = LinkedQueue()  # 先生成L\E\G这三个队列
  while not S.is_empty():             # divide S into L, E, and G
    if S.first() < p:
      L.enqueue(S.dequeue())  # 小 左 L
    elif p < S.first():
      G.enqueue(S.dequeue())  # 大 右 G
    else:                             # S.first() must equal pivot
      E.enqueue(S.dequeue())   # 中 等 E
  # conquer (with recursion)
  quick_sort(L)                       # sort elements less than p
  quick_sort(G)                       # sort elements greater than p # 递归对L G进行分解和快速排序
  # concatenate results
  while not L.is_empty():
    S.enqueue(L.dequeue())
  while not E.is_empty():
    S.enqueue(E.dequeue())
  while not G.is_empty():
    S.enqueue(G.dequeue())      # 最终进行串联

快速排序的时间分析:需要确认在快速排序树T上的每个节点的时间开销,并求出所有节点的运行时间总和。可以看到分解步骤和快速排序的最终串联可以在线性时间内实现。如果基准值能够均分S序列,则整体的时间消耗是O(nlogn);如果出现选中了最大值最为基准值,则整个的时间消耗是O(n^2),当然这是最坏的情况。所以基准值的选择更为重要。

12.3.1 随机快速排序

分析快速排序的一般方法是假设基准值总是能将序列以合理、平衡的方式分解。一般而言,我们希望一些方法可以使快速排序的运行时间更接近最好情况的运行时间。当然,这种接近最优化运行时间的方法,就是使得基准值近乎平均分输入序列S。如果这一结果发生,将导致运行时间趋于最好的运行时间。也就是说,让基准值尽量接近元素集合的‘中间’,会使快速排序的运行时间达到O(nlogn)

命题:一个大小为n的序列S,其随机化快速排序的期望运行时间是O(n*logn)。

12.3.2 快速排序的额外优化

对于一个算法而言,如果他除了原始所需的内存外,仅仅只使用少量的内存,则该算法就是就地算法。就地算法对于所有的递归调用,我们必须使用输入序列本身来存储其子序列。我们给出执行就地快速排序的算法inplace_quick_sort,其假设输入序列S的元素是以python list的形式呈现的。就地快速排序通过使用元素交换的方法改变输入序列,并且隐式地创建新的子序列。相反,输入序列的子序列却隐式地通过一个被左索引a和最右索引b所指定的位置范围表示出来。分解步骤是通过使用向前移动的本地变量left和向后移动的本地变量right同时扫描数组,并交换逆序的元素对实现的。当left和right相遇时,分解的步骤就完成了,并且该算法会在这两个子序列上递归完成。

def inplace_quick_sort(S, a, b):
  """Sort the list from S[a] to S[b] inclusive using the quick-sort algorithm."""
  if a >= b: return                                      # range is trivially sorted
  pivot = S[b]                                           # last element of range is pivot
  left = a                                               # will scan rightward
  right = b-1                                            # will scan leftward
  while left <= right:
    # scan until reaching value equal or larger than pivot (or right marker)
    while left <= right and S[left] < pivot:
      left += 1
    # scan until reaching value equal or smaller than pivot (or left marker)
    while left <= right and pivot < S[right]:
      right -= 1
    if left <= right:                                    # scans did not strictly cross
      S[left], S[right] = S[right], S[left]              # swap values
      left, right = left + 1, right - 1                  # shrink range

  # put pivot into its final place (currently marked by left index)
  S[left], S[b] = S[b], S[left]
  # make recursive calls
  inplace_quick_sort(S, a, left - 1)
  inplace_quick_sort(S, left + 1, b)

12.4 再论排序:算法视角

12.4.1 排序下界

基于比较的排序算法在最坏的情况下有\Omega (n* logn)的运行时间下界。

命题:任何基于比较的排序算法对有n个元素的序列排序所花费的时间都是\Omega (n* logn)

12.4.2 线性时间排序:桶排序和基数排序

桶排序

基数排序:如果有两个条目,是先按照第一条目进行排序,再按照第二个条目进行排序还是先按照第二个,再按照第一个,实验表明:先按照第二个条目排序,然后基于排序的结果再按照第一个条目进行排序的结果更与自然顺序更贴近。

基数排序可以应用于任何键都可以看作以字典序排序得得到的小规模排序的情形。

稳定排序:对于S中任意两个条目(k_i,v_i)(k_j,v_j)k_i = k_j并且排序前(k_i,v_i)(k_j,v_j)前面,则排序后(k_i,v_i)仍然在前面。一个排序算法的稳定性很重要的,因为应用程序或许想用相同的键保留原始顺序。

12.5 排序算法的比较

插入排序  bset: O(n+m),m是逆序的数量;worse:O(n^2)。插入算法是一种小序列进行排序的优秀算法。对几乎已经排好序的序列是非常有效的,这里的几乎是指的逆序数目非常小。但是插入排序O(n^2)的时间性能使它在特定的情况之外称为了一种糟糕的选择。

堆排序:堆排序在最坏的情况下运行时间是为\Omega (n* logn),对于基于比较的排序方法是最佳的选择。不稳定的排序算法;但是在更大的序列上往往优于快速排序和归并排序。

快速排序:最坏情况下的时间复杂度是O(n^2)。不稳定的排序算法;优于归并算法。

归并排序:最坏情况下的时间复杂度是\Omega (n* logn)。对于输入在计算机的各级存储器层次结构(例如:高速缓存、主存储器、外部存储器)之间被分层的情况,归并排序仍然是一个优秀的算法。

桶排序和基数排序:如果一个应用程序小的整数键、字符串或者来自离散范围的d元组键对条目进行排序,那么桶排序和基数排序是最好的选择。因为它的运行时间为O(d(n+N)).因此,如果d(n+N)明显低于nlogn的函数,那么这个分类方法要比快速排序、堆排序、归并排序更快。

12.6 python的内置排序函数

python中涉及到排序的函数有两个:sort()和sorted().但是对于采用那一种键函数是可以通过key进行自动选择的,例如:如果根据字符串的长度进行排序,则可以设置key=len,reverse的默认是升序排列,reverse=True的话,则是采用降序排列进行展示。

抵用装饰-排序-取消设计模式实现排序时。可以支持键函数。

下面是基于数组的归并排序的装饰-排序-取消设计模式的实现方法。_Item类和PriorityQueueBase类中的使用相同。

from .merge_array import merge_sort

class _Item:
  """Lightweight composite to store decorated value for sorting."""
  __slots__ = '_key', '_value'

  def __init__(self, k, v):
    self._key = k
    self._value = v

  def __lt__(self, other):
    return self._key < other._key    # compare items based on their keys

def decorated_merge_sort(data, key=None):
  """Demonstration of the decorate-sort-undecorate pattern."""
  if key is not None:
    for j in range(len(data)):
      data[j] = _Item(key(data[j]), data[j])          # decorate each element
  merge_sort(data)                                    # sort with existing algorithm
  if key is not None:
    for j in range(len(data)):
      data[j] = data[j]._value                        # undecorate each element

12.7 选择

对元素集合所要处理的各种顺序关系来说,排序不是唯一有趣的问题。对于大量的应用,相对于整个集合的排序顺序,我们对根据元素的级别来识别单个元素更感兴趣。比如最小数、最大数、中位数等等

定义选择问题

一般的顺序统计量,即从为未排序的n个可比较元素中选择第k个最小的元素,这样的被称为选择问题。

随机快速选择:对n个元素的未排序序列应用剪枝搜索模式去寻找第k个最小的元素时,我们用到一种简单实用的算法,称为随机快速选择。

import random

def quick_select(S, k):
  """Return the kth smallest element of list S, for k from 1 to len(S)."""
  if len(S) == 1:
    return S[0]
  pivot = random.choice(S)             # pick random pivot element from S
  L = [x for x in S if x < pivot]      # elements less than pivot
  E = [x for x in S if x == pivot]     # elements equal to pivot
  G = [x for x in S if pivot < x]      # elements greater than pivot
  if k <= len(L):
    return quick_select(L, k)          # kth smallest lies in L
  elif k <= len(L) + len(E):
    return pivot                       # kth smallest equal to pivot
  else:
    j = k - len(L) - len(E)            # new selection parameter
    return quick_select(G, j)          # kth smallest is jth in G

随机快速选择的分析:

大小为n的序列S的随机快速选择的预期运行时间是O(n),假设S的两个元素可以在O(1)时间内进行比较。

 

 

 

 

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值