笔记-算法-排序算法
笔记-算法-排序算法
1. 排序算法
1.1. 排序算法分类
1. 算法稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
2.   内排序和外排序
内排序:排序过程中,待排序的所有记录全部放在内存中
外排序:排序过程中,使用到了外部存储。
3. 影响内排序算法性能的三个因素:
时间复杂度:即时间性能,高效率的排序算法应该是具有尽可能少的关键字比较次数和记录的移动次数
空间复杂度:主要是执行算法所需要的辅助空间,越少越好。
算法复杂性。主要是指代码的复杂性。
4. 根据排序过程中借助的主要操作,可把内排序分为:
插入排序,交换排序,选择排序,归并排序
5. 按照算法复杂度可分为两类:
简单算法:包括冒泡排序、简单选择排序和直接插入排序
改进算法:包括希尔排序、堆排序、归并排序和快速排序
1.2. 常见排序算法分类
 
2. 排序算法性能
 
3. 算法实现
3.1. 插入排序
思路:将新元素插入已排序序列,一般来说,插入排序都采用in-place在数组上实现。
时间复杂度:O(n2)
空间复杂度:O(1)
稳定性:稳定
def insert_sort(lis):
for i in range(1, len(lis)):
for j in range(i):
if lis[i] < lis[j]:
lis.insert(j, lis.pop(i))
break
return lis
3.2. 希尔排序
思路:把数组按下标的一定增量分组,对每组使用插入排序算法排序;增量逐渐减少,当增量减至1时,算法终止。
希尔排序的时间复杂度与增量序列的选取有关,希尔增量时间复杂度为O(n²),而Hibbard增量的希尔排序的时间复杂度为O()
希尔排序没有快速排序算法快 O(n(logn)),因此中等大小规模表现良好,对规模非常大的数据排序不是最优选择。但是比O()复杂度的算法快得多。并且希尔排序非常容易实现,算法代码短而简单。
希尔算法在最坏的情况下和平均情况下执行效率相差不是很多,与此同时快速排序在最坏的情况下执行的效率会非常差。 本质上讲,希尔排序算法是直接插入排序算法的一种改进,减少了其复制的次数,速度要快很多。 原因是,当n值很大时数据项每一趟排序需要的个数很少,但数据项的距离很长。当n值减小时每一趟需要和动的数据增多,此时已经接近于它们排序后的最终位置。 正是这两种情况的结合才使希尔排序效率比插入排序高很多。
算法稳定性:不稳定
def shell_sort(lis):
gap = len(lis)
while gap > 1:
gap = gap // 2
for i in range(gap, len(lis)):
for j in range(i % gap, i, gap):
if lis[i] < lis[j]:
lis[i], lis[j] = lis[j], lis[i]
return lis
上面的排序是非常慢的,因为第三层循环比较次数和交换次数控制不力;
#希尔排序(增量序列为希尔增量)
def shell_sort(lis):
gap = len(lis)
while gap > 1:
gap = gap // 2
for i in range(gap, len(lis)):
j = i
while j>=gap and lis[j] < lis[j-gap]:
lis[j], lis[j-gap] = lis[j-gap], lis[j]
j -=gap
return lis
下面使用另一种增量方式。
def shell_sort(lis):
#assert 0,print("www")
n = len(lis)
h = 1
while h < n/3:
h = h*3 + 1
while h >=1:
for i in range(h,n):
j = i
while j >= h and lis[j] < lis[j-h]:
lis[j], lis[j-h] = lis[j-h], lis[j]
j -= h
h = h//3
return lis
3.3. 简单选择排序
思路:在无序序列中选择最小的一个元素,然后添加到有序序列尾部。在比较时记录元素下标,最后交换。
时间复杂度:O(n²)
空间复杂度:O(1)
稳定性:不稳定
#简单选择排序
def select_sort(lis):
for i in range(len(lis)):
x = i
for j in range(i, len(lis)):
if lis[i] < lis[j]:
x = j
lis[i], lis[x] = lis[x], lis[i]
return lis
3.4. 堆排序
堆是具有下列性质的完全二叉树:
每个分支节点的值都大于或等于其左右孩子的值,称为大顶堆;
每个分支节点的值都小于或等于其左右孩子的值,称为小顶堆;
如果给上面的大小顶堆的根节点从1开始编号,则满足下面关系:
小顶堆:K[i]<=K[2i],K[i]<=K[2i+1]
大顶堆:K[i]>=K[2i],K[i]>=K[2i+1]
堆排序核心思想是:将待排序的序列构成一个大顶堆。此时,整个序列的最大值就是堆的根节点,将它与堆数组的末尾元素交换,然后将剩余n-1个序列重新构造成一个大顶堆。反复执行前述操作,最后获得一个有序序列。
时间复杂度:O(n*logn) 最坏时间复杂度和最优时间复杂度一样;
空间复杂度:O(1)
稳定性:不稳定
#堆排序
def sift_down(array, start, end):
"""
构造大顶堆,初始堆时,从下往上;交换堆顶与堆尾后,从上往下调整;
注意边界条件控制,
以及子节点调整后可能导致孙节点不符合大顶堆的要求,需要补充调整
:param array:列表的引用
:param start:父节点
:param end:结束的下标
:return:无
"""
root = start
while True:
child = 2 * root + 1
if child > end:
break
if child + 1 <= end and array[child] < array[child + 1]:
child += 1
if array[root] < array[child]:
array[root], array[child] = array[child], array[root]
root = child
else:
break
def heap_sort(array):
#初始化大顶堆
first = len(array)//2 - 1
for i in range(first, -1, -1):
sift_down(array, i, len(array) -1)
#已构造大顶堆,将堆顶array[0]与堆尾array[-1]互换,则array[-1]是有
序的,对array[:-1]继续构造大顶堆,互换首尾,重复得到结果
for i in range(len(array)-1, 0, -1):
array[0], array[i] = array[i], array[0]
sift_down(array, 0, i -1)
return array
3.5. 冒泡排序
思路:二层循环,一层控制循环次数,二层将最大元素后调。
时间复杂度:O(N2)
空间复杂度:O(1)
稳定性:稳定
#bubble_sort
def bubble_sort(array):
for i in range(len(array)-1):
for j in range(len(array)-i-1):
if array[j] > array[j+1]:
array[j], array[j+1] = array[j+1], array[j]
return array
3.6. 快速排序
时间复杂度:O(nlog2 n)
空间复杂度:O(nlog2 n)
稳定性:不稳定
理解:分治法,选定某一个元素(一般是最左端元素),大于它的放在右侧,小于它的放在右侧,它放中间;好了,一个元素的位置确定了,对该元素左右分别递归操作。
为什么比堆快?1.数据无效移动少一些;2.在大规模排序时,快速排序的数据存取是顺序的,而堆是随机的,存取时间短。
def quickSort(array):
if len(array) < 2: # 基线条件(停止递归的条件)
return array
else: # 递归条件
baseValue = array[0] # 选择基准值
# 由所有小于基准值的元素组成的子数组
less = [m for m in array[1:] if m < baseValue]
# 包括基准在内的同时和基准相等的元素,在上一个版本的百科当中,并没有考虑相等元素
equal = [w for w in array if w == baseValue]
# 由所有大于基准值的元素组成的子数组
greater = [n for n in array[1:] if n > baseValue]
return quickSort(less) + equal + quickSort(greater)
下面是一个in-place例子:
# quick_sort
def quick_sort(array):
def recursive(begin, end):
if begin >= end:
return
left, right = begin, end
key = array[left]
while left < right:
while left < right and array[right] > key:
r -= 1
while left < right and array[left] <= key:
left += 1
array[left], array[right] = array[right], array[left]
#上面三个循环的作用是左右指针使得数组存在某一个点,在这个点左侧的元素一定小于等于key,右侧的元素一定大于key;
#最后left=right且一定指向一个小于等于kye的元素
array[left], array[begin] = key, array[left]
#key左右的数组分别递归
recursive(begin, left -1)
recursive(right + 1, end)
recursive(0, len(array)-1)
return array
python有最大递归次数限制,再来一个非递归的实现吧:
基本思路是使用队列保存下标组。
def quick_sort1(lis):
def recursive(begin, end):
if begin >= end:
return begin
left, right = begin, end
key = array[left]
while left < right:
while left < right and array[right] > key:
right -= 1
while left < right and array[left] <= key:
left += 1
array[left], array[right] = array[right], array[left]
array[left], array[begin] = key, array[left]
return left
if len(lis) < 2:
return lis
length = len(lis)
stack = []
stack.append([0,length-1])
while stack:
l, r = stack.pop()
mid = recursive(l,r)
if mid + 1 < r:
stack.append([mid+1,r])
if mid - 1 > l:
stack.append([l, mid-1])
return lis
3.7. 归并排序
思路:类似二叉树不断有序合并;
时间复杂度:O(nlog2 n)
空间复杂度:O(1)
稳定性:稳定
# merge_sort
def merge_sort(array):
def merge_arr(arr_l, arr_r):
array_temp = []
while len(arr_l) and len(arr_r):
if arr_l[0] <= arr_r[0]:
array_temp.append(arr_l.pop(0))
else:
array_temp.append(arr_r.pop(0))
array_temp += arr_l
array_temp += arr_r
return array_temp
def recursive(array):
if len(array) == 1:
return array
mid = len(array) // 2
arr_l = recursive(array[:mid])
arr_r = recursive(array[mid:])
return merge_arr(arr_l, arr_r)
return recursive(array)
3.8 计数排序
思路:对于n个0-k之间的整数,对每一个数的元素x,确定出小于x的元素个数。有了这一信息就可以把x直接放到最终输出数组中的位置上
时间复杂度:最好情况下为O(n+k),最坏情况下为O(n+k),平均情况为O(n+k);
空间复杂度为O(n+k)
稳定性:稳定
def counting_sort(lis, k):
n = len(lis)
result = [None]*n
lis_c = [0]*k
for i in lis:
lis_c[i] += 1
for i in range(1,k+1):
lis_c[i] = lis_c[i] + lis_c[i-1]
for i in range(n,-1,-1):
result[lis_c[lis[i]]-1] = lis[i]
lis_c[lis[i]] -= 1
return result
疑问:lis_c包含所有元素的大小及个数,为什么不直接生成一个数组,而是要去原数组中循环。
3.9桶排序
思路:桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。
算法描述:
设置一个定量的数组当作空桶;
遍历输入数据,并且把数据一个一个放到对应的桶里去;
对每个不是空的桶进行排序;
从不是空的桶里把排好序的数据拼接起来。
桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。
# bucket sort
def bucket_sort(lis):
num_min = min(lis)
buckets = [0] *(max(lis) - min(lis) + 1)
for i in range(len(lis)):
buckets[lis[i]-num_min] += 1
res = []
for i,j in enumerate(buckets):
if j != 0
res.extend([i + num_min]*j)
return res
3.10. 基数排序
时间复杂度:O(b(n+r)) ,其中n为数据集中元素个数,r为基数radix,b为每个元素的位数bits;
空间复杂度:需要额外的temp和counts,大致可以记为O(n)。
算法思想:基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。
算法描述
取得数组中的最大数,并取得位数;
arr为原始数组,从最低位开始取每个位组成radix数组;
对radix进行计数排序(利用计数排序适用于小范围数的特点);
# radix_sort(array):
def radix_sort(array):
bucket, digit = [[]], 0
while len(bucket[0]) != len(array):
bucket =[[] for j in range(10)]
for i in range(len(array)):
num = (array[i] // 10 ** digit) % 10
bucket[num].append(array[1])
array.clear()
for i in range(len(bucket)):
array += bucket[i]
digit += 1
return array
 
                     
                    
                 
                    
                
 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号