1.算法概述
1.1.算法分类
- 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
- 非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序
1.2.重要概念
- 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
- 不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
- 时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
- 空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数
- 本文约定:实现从小到大排序
1.3.算法复杂度
#插入排序
def instertion_sort(list_a):
len_l = len(list_a)
for i in range(1,len_l):
index_now = i #用于存储当前选择比较值的序列
n = i - 1
#每次扫描,必定有元素在最终位置
while n >= 0 and list_a[n] > list_a[index_now]:
list_a[n],list_a[index_now] = list_a[index_now],list_a[n]
index_now = n
n = n - 1
return list_a
2.冒泡排序
2.1.算法描述
- 比较相邻的元素,如果两元素逆序,交换两元素位置
- 从第一对元素开始,对每对元素执行上述操作,直到最后一对元素,此时最大元素在最后一位
- 对未经此过程排序的元素,执行上述方式,直至排序完成
特点:每对list遍历一次,必定有元素停留在最终位置
2.2.动图演示
2.3.代码实现
def bubble_sort(list_a):
len_l = len(list_a)
for i in range(len_l - 1): #遍历n-1次
for j in range(len_l - i - 1): #对完成排序的元素不再比较
if(list_a[j] > list_a[j+1]):
temp = list_a[j]
list_a[j] = list_a[j+1]
list_a[j+1] = temp
#python中元素交换值可用 a,b = b,a
#list_a[j],list_a[j+1] = list_a[j+1],list_a[j]
return list_a
2.4.算法优化
def bubble_sort_optimize(list_a):
len_l = len(list_a) #挺多余的
for i in range(len_l - 1):
flag = True #优化的冒泡排序额,添加的哨兵,初始值为True
for j in range(len_l - i - 1):
if(list_a[j] > list_a[j+1]):
flag = False #如果对于所有的值,都有list[i] < list[i+1],哨兵变为True
list_a[j],list_a[j+1] = list_a[j+1],list_a[j]
if flag: #如果flag == True,说明任何相邻两元素都没有逆序,列表由小到大顺序排列
break
return list_a
2.5. 算法分析
以最差情况(元素全部逆序)为例:
第一次比较n-1次,最后一次比较1次,总的比较次数,平均复杂度为
优化之后,顺序为最好情况,比较n-1次,最优时间复杂度为
3.快速排序
3.1.算法描述
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:
- 从数列中挑出一个元素,称为 “基准”(pivot);
- 分区(partition)操作:所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。
- 递归(recursive):对小于基准值元素的子数列和大于基准值元素的子数列重复上述两个操作,直到完成最终只含一个元素
3.2.动图演示
3.3.代码实现
算法1:
#这种写法的平均空间复杂度为 O(nlogn)
def quick_sort(list_a):
if len(list_a) <= 1: #递归出口
return list_a
my_pivot = list_a[0] #基准值选取
left = []
right = []
for i in range(1,len(list_a)):
if list_a[i] <= my_pivot: #分区操作,比基准值小的元素
left.append(list_a[i])
if list_a[i] > my_pivot: #分区操作,比基准值大的元素
right.append(list_a[i])
return quick_sort(left) + [my_pivot] + quick_sort(right) #递归
算法2:
# 这种写法的平均空间复杂度为 O(logn)
def quick_sort(my_list,left,right):
# 递归操作
if left < right:
pivot_index = partition(my_list, left, right)
quick_sort(my_list, left, pivot_index - 1) # 左序列
quick_sort(my_list, pivot_index + 1, right) # 右序列
return my_list
def partition(my_list, left, right):
pivot = my_list[left] # 基准值
while left < right:
#如果列表后边的数,比基准数大或相等,则前移一位直到有比基准数小的数出现
while left < right and my_list[right] >= pivot:
right -= 1
my_list[left] = my_list[right] # 比基准小的交换到前面
#同样的方式比较前半区
while left < right and my_list[left] <= pivot:
left += 1
my_list[right] = my_list[left] # 比基准大交换到后面
my_list[left] = pivot # 基准值的正确位置,也可以为 my_list[right] = pivot
return left # 返回基准值的索引,也可以为 return right
3.4.算法分析
时间效率:快速排序的运行时间与划分是否对称有关,而后者又与具体划分算法有关。最坏情况为逆序,两个区域分别包含n-1个和0个元素。每次分区分别比较n-1,n-2,...,1。最终比较次数,因此最坏情况下,时间复杂度为
快速是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。它是处理大数据最快的排序算法之一,虽然 最坏案例情况下 的时间复杂度达到了 O(n²),但是在大多数情况下都比平均时间复杂度为 O(n log n) 的排序算法表现要更好,因为 O(n log n) 记号中隐含的常数因子很小,而且快速排序的内循环比大多数排序算法都要短小,这意味着它无论是在理论上还是在实际中都要更快,比复杂度稳定等于 O(n log n) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。它的主要缺点是非常脆弱,在实现时要非常小心才能避免低劣的性能。
4.简单插入排序
同打扑克牌类似,每次将元素插入到指定位置
4.1.算法描述
- 从第二个元素开始,依次取出元素,进行如下操作
- 扫描该元素之前有序列表段,将该有序列表段元素,从后往前依次与该元素比较
- 如果逆序,列表段元素后移,否则插入取出元素
特点:每对list遍历一次,必定有元素停留在最终位置
4.2.动图演示
4.3.代码实现
算法1:
#插入排序优化
def instertion_sort_optimize(list_a):
len_l = len(list_a)
for i in range(1,len_l):
save_var = list_a[i] #存储要比较的值
n = i - 1
#每次扫描,必定有元素在最终位置
while n >= 0 and list_a[n] > save_var:
list_a[n+1] = list_a[n]
n = n - 1
list_a[n+1] = save_var
return list_a
算法2:
#插入排序
def instertion_sort(list_a):
len_l = len(list_a)
for i in range(1,len_l):
index_now = i #用于存储当前选择比较值的序列
n = i - 1
#每次扫描,必定有元素在最终位置
while n >= 0 and list_a[n] > list_a[index_now]:
list_a[n],list_a[index_now]= list_a[index_now],list_a[n]
index_now = n
n = n - 1
return list_a
4.4.算法分析
以最差情况(元素全部逆序)为例:
第一次比较n-1次,最后一次比较1次,总的比较次数,平均时间复杂度为
5.希尔排序
1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。
5.1.算法描述
- 设定步长(为列表长的一半,设为d,其中d>=1)
- 以步长为标准,每个步长个元素为一组,对列表进行分组(如:1,1+d,1+2d...)
- 分别对分组进行排序
- 步长减半,重复2、3两步
5.2.动图演示
5.3.代码实现
#希尔排序
def shell_sort(list_a):
d = int(len(list_a)/2) #设置初始步长
while d >= 1: #步长极限值
#分组执行排序操作,此处选用优化的插入排序
for i in range(d,len(list_a)):
j = i - d
temp = list_a[i]
while j >= 0 and temp < list_a[j]:
list_a[j+d] = list_a[j]
j = j -d
list_a[j+d] = temp;
d = int(d/2) #步长减半
return list_a
5.4.算法分析
希尔排序的核心在于间隔序列的设定。既可以提前设定好间隔序列,也可以动态的定义间隔序列。
6.选择排序
6.1.算法描述
- 从第一个元素开始,依次指定元素为待定序元素,并进行如下三个操作
- 扫描列表未排序段,同该元素之后的每个元素相比较
- 如果逆序,则更换待定序元素
- 将交换后的新元素同剩余元素作比较,重复第三个步骤,直至表尾
特点:每对list遍历一次,必定有元素停留在最终位置
6.2.动图演示
6.3.代码实现
#选择排序
def selection_sort(list_a):
len_l = len(list_a)
for i in range(len_l - 1):
min_index = i
for j in range(i+1,len_l):
if list_a[min_index] > list_a[j]: #寻找最小值
min_index = j #保存最小值索引
if min_index != i:
temp = list_a[i]
list_a[i] = list_a[min_index]
list_a[min_index] = temp
return list_a
6.4.算法分析
表现最稳定的排序算法之一,因为无论什么数据进去都是O(n2)的时间复杂度,所以用到它的时候,数据规模越小越好。
7.堆排序
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点
7.1.算法描述
- 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
- 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
- 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
7.2.动图演示
7.3.代码实现
# 大根堆(从小打大排列)
def heapSort(nums):
# 调整堆
def adjustHeap(nums, i, size):
# 非叶子结点的左右两个孩子
lchild = 2 * i + 1
rchild = 2 * i + 2
# 在当前结点、左孩子、右孩子中找到最大元素的索引
largest = i
if lchild < size and nums[lchild] > nums[largest]:
largest = lchild
if rchild < size and nums[rchild] > nums[largest]:
largest = rchild
# 如果最大元素的索引不是当前结点,把大的结点交换到上面,继续调整堆
if largest != i:
nums[largest], nums[i] = nums[i], nums[largest]
# 第 2 个参数传入 largest 的索引是交换前大数字对应的索引
# 交换后该索引对应的是小数字,应该把该小数字向下调整
adjustHeap(nums, largest, size)
# 建立堆
def builtHeap(nums, size):
for i in range(len(nums)//2)[::-1]: # 从倒数第一个非叶子结点开始建立大根堆
adjustHeap(nums, i, size) # 对所有非叶子结点进行堆的调整
print(nums) # 第一次建立好的大根堆
# 堆排序
size = len(nums)
builtHeap(nums, size)
for i in range(len(nums))[::-1]:
# 每次根结点都是最大的数,最大数放到后面
nums[0], nums[i] = nums[i], nums[0]
# 交换完后还需要继续调整堆,只需调整根节点,此时数组的 size 不包括已经排序好的数
adjustHeap(nums, 0, i)
return nums # 由于每次大的都会放到后面,因此最后的 nums 是从小到大排列
8.二路归并排序
采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
8.1.算法描述
- 将长度为n的列表分为2个长度n/2的子序列
- 分别对两个子序列调用归并排序
- 将两个排序好的子序列合并成一个最终的排序序列
归并出口: 子序列长度小于2
8.2.动图演示
8.3.代码实现
def merge_sort(list_a):
len_l = len(list_a)
if len_l < 2:
return list_a
middle = int(len_l/2)
list_left = merge_sort(list_a[:middle])
list_right = merge_sort(list_a[middle:])
return merge(list_left,list_right)
def merge(list_left,list_right):
result = []
i = j = 0
while i < len(list_left) and j < len(list_right):
if list_left[i] < list_right[j]:
result.append(list_left[i])
i = i + 1
else:
result.append(list_right[j])
j = j + 1
result = result + list_left[i:] + list_right[j:]
return result
8.4.算法分析
归并排序是一种稳定的排序方法。和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)的时间复杂度。代价是需要额外的内存空间。
9.计算排序
9.1.算法描述
- 找出待排序的列表中最大和最小的元素;
- 统计列表中每个值为i的元素出现的次数,存入列表C的第i项;
- 对所有的计数累加;
- 反向填充目标列表:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。
9.2.动图演示
9.3.代码实现
#计数排序
def counting_sort(list_a):
#生成桶
buckets = [0 for i in range(max(list_a)+1)]
#计数
for i in list_a:
buckets[i] += 1
i = 0
#取出
for j in range(len(buckets)):
while buckets[j] > 0:
list_a[i] = j
buckets[j] -= 1
i += 1
return list_a
9.4.算法分析
计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
计数排序是一个稳定的排序算法。当输入的元素是 n 个 0到 k 之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。
10.桶排序
10.1.算法描述
类比: 类似于C语言中链表
- 设置一个定量的列表(列表的元素是列表)当作空桶;
- 遍历输入数据,并且把数据一个一个放到对应的桶里去;
- 对每个不是空的桶进行排序;
- 从不是空的桶里把排好序的数据拼接起来。
10.2.动图演示
10.3.代码实现
#桶排序
def bucket_sort(list_a,default_bucket_count = 5):
max_item = max(list_a)
min_item = min(list_a)
bucket = [[] for i in range(default_bucket_count)] #初始化桶[[],[],...,[]]
bucket_size = math.ceil(((max_item - min_item)/(default_bucket_count-1)))
for item in list_a:
index_bucket = (item - min_item)//bucket_size
bucket[index_bucket].append(item) #此处可以考虑将排序放入
pos = 0
#清空桶,将元素填充到目标list中
for arr in bucket:
arr.sort() #对每个桶进行排序
if arr != []:
for i in arr:
list_a[pos]= i
pos = pos + 1
return list_a
10.4.算法分析
为了使桶排序更加高效,我们需要做到这两点:
1、在额外空间充足的情况下,尽量增大桶的数量
2、使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。
最快:当输入的数据可以均匀的分配到每一个桶中
最慢:当输入的数据被分配到了同一个桶中
11.基数排序排序
11.1.算法描述
- 找出列表中最大元素(如333)
- 比较个位,将元素排序
- 比较十位,将元素排序
- 比较百位,将元素排序
11.2.动图演示
11.3.代码实现
#基数排序
def radix_sort(list_a):
mod = 10
dev = 1
max_digit = len(str(max(list_a))) #求数字中最大长度
for i in range(max_digit):
counter = [[] for a in range(10)] #创建储存单元
for item in list_a:
'''x/y 除,x与y之商 10/3=3.3333333…5
x//y 整数除,x与y之整数商 10//3 = 3
x%y 余数,模运算, 10%3 = 1'''
index_bucket = item%mod//dev
counter[index_bucket].append(item)
pos = 0
for arr in counter:
if arr != []:
for item in arr:
list_a[pos] = item
pos = pos + 1
mod = mod * 10
dev = dev * 10
return list_a
11.4.算法分析
基数排序基于分别排序,分别收集,所以是稳定的。但基数排序的性能比桶排序要略差,每一次关键字的桶分配都需要O(n)的时间复杂度,而且分配之后得到新的关键字序列又需要O(n)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2n) ,当然d要远远小于n,因此基本上还是线性级别的。基数排序的空间复杂度为O(n+k),其中k为桶的数量。
参考链接:https://www.cnblogs.com/onepixel/articles/7674659.html#!comments