算法效率和初等排序
作者:互联网
借鉴自:算法专栏
目录
一、时间复杂度
1.算法效率
算法效率的评估:时间复杂度和空间复杂度
空间复杂度:执行程序所需要的存储空间,算法程序对计算机内存的使用情况
时间复杂度:不是表示一个程序解决问题具体需要花多少时间,而是当问题规模扩大后,程序需要的时间长度增长的有多快。衡量一个程序的好坏要看数据规模变大到数百倍后程序的运行时间是如何变化的。
设计算法是首先考虑系统环境,然后权衡时间复杂度和空间复杂度,寻找一个平衡点,而时间复杂度往往比空间复杂度更容易产生问题,因此时间复杂度是算法研发的主要部分。
2.时间复杂度
时间频度f(n):一个算法执行所需要的时间,理论上是不能计算出来的,必须上机测试才能知道,而算法中语句的执行次数与算法花费时间成正比,执行次数多花费时间多,因此通常通过计算算法中语句的执行次数来计算时间复杂度。算法中语句的执行次数即T(n),n是问题的规模,n不断变化时,T(n)也会不断变化,而时间复杂度就是这种变化呈现的规律。
时间复杂度的计算:
大O表示法,O(f(n))中f(n)的值可以是1,n,log(n),n^2等,因此将O(1),O(n),O(logn),O(n^2)分别称为常数阶、线性阶、对数阶、平方阶。
推导f(n)值的规则:
(1).用常数1代替运行次数中所有加法常数
(2).f(n)只保留最高阶项
(3).如果最高阶项存在且不是1,就去掉与这项相乘的常数
常数阶
int sum = 0,n = 100; //执行一次 sum = (1+n)*n/2; //执行一次 System.out.println (sum); //执行一次
上面例子中f(n)=3,根据规则一,此算法的时间复杂度为O(1)。如果sum = (1+n)*n/2这条语句再执行10遍,执行的10次与n的大小并没有关系,因此算法时间复杂度依然是O(1)。
线性阶
主要分析循环结构的运行情况
for(int i=0;i<n;i++){ //时间复杂度为O(1)的算法 ... }
上面算法循环体中的代码执行了n次,故时间复杂度为O(n)
对数阶
int number=1; while(number<n){ number=number*2; //时间复杂度为O(1)的算法 ... }
上面代码中,number每次乘2就会越来越接近n,当number大于等于n时就会退出循环。假设循环次数为x,则由2^x=n可得x=log n ,所以此算法的时间复杂度为O(log n)。
平方阶
下面代码是循环嵌套
for(int i=0;i<n;i++){ for(int j=0;j<n;i++){ //复杂度为O(1)的算法 ... } }
内部循环代码的时间复杂度为O(n),再经过外部循环n次,所以此算法的时间复杂度是O(n^2)。
再看下面的算法:
for(int i=0;i<n;i++){ for(int j=i;j<n;i++){ //复杂度为O(1)的算法 ... } }
注意内循环中变成了j=i,那么当i=0时,内循环执行了n次;i=1时,内循环执行了n-1次;i=2时,内循环执行了n-2次....以此类推,总共的循环执行次数为:
n+(n-1)+(n-2)+(n-3)+……+1
=(n+1)+[(n-1)+2]+[(n-2)+3]+[(n-3)+4]+……
=(n+1)+(n+1)+(n+1)+(n+1)+……
=(n+1)n/2
=n(n+1)/2
=n²/2+n/2根据规则2只保留最高阶项和规则3去掉常数项,所以此算法的时间复杂度为O(n^2)。
3.时间复杂度的比较
除了常数阶、线性阶、平方阶、对数阶,还有如下时间复杂度:
f(n)=nlogn时,时间复杂度为O(nlogn),可以称为nlogn阶。
f(n)=n³时,时间复杂度为O(n³),可以称为立方阶。
f(n)=2ⁿ时,时间复杂度为O(2ⁿ),可以称为指数阶。
f(n)=n!时,时间复杂度为O(n!),可以称为阶乘阶。
f(n)=(√n时,时间复杂度为O(√n),可以称为平方根阶。
时间复杂度为O(n)、O(logn)、O(√n )、O(nlogn )的算法,随着n的增加,复杂度提升不大,因此这些复杂度属于效率高的算法;反观O(2ⁿ)和O(n!)当n增加到50时,复杂度就突破十位数了,这种效率极差的复杂度最好不要出现在程序中,因此在动手编程时要评估所写算法的最坏情况的复杂度。
横坐标是 n,纵坐标是T(n),T(n)随着n的变化越小,算法效率就越高。
二、初等排序
1.插入排序
插入排序会将需要排序的数组分成两个部分,已排序部分和未排序部分。排序的规则即:将开头元素视为已排序部分,从未排序部分开始,将开头元素赋值给临时变量v,在已排序部分将所有比v值大的元素向后移动一位,将元素v插入空位。
比如对数组 a={8,3,1,5,2,1} 进行从小到大排序,数组a如下图所示:
排序过程:
(1).将a[0]=8视为已排序部分,将a[1]=3的值取出赋值给v,得到v=3,v<a[0],将a[0]向后移动一位到a[1],将v=3插入到原来a[0]的位置。
(2).a[0]和a[1]是已排序部分,将a[2]=1赋值给v,v=1小于a[0]和a[1],因此将a[1]和a[0]依次后移一位,将v=1插入到a[0]的位置。
(3).后面一次按照相同规则对a[3]、a[4]、a[5]进行排序。
插入排序的python代码实现:
v是临时赋值变量,i是未排序部分的开头元素,j是已排序部分的末尾元素。算法思路:外循环i从1开始自增,每次循环都将a[i]赋值给v;内循环从j=i-1开始自减,依次比较a[j]与v的大小,比v小就依次向后移动一位,当j=-1或a[j]小于等于v循环结束。
a = [8, 3, 1, 5, 2, 1]
print('原序列:',a)
n = len(a)
for i in range(1, n):
v = a[i]
j = i - 1
while j >= 0 and v < a[j]:
a[j+1] = a[j]
j = j - 1
a[j+1] = v
print('插入排序后:',a)
输出结果:
原序列: [8, 3, 1, 5, 2, 1]
插入排序后: [1, 1, 2, 3, 5, 8]
插入排序的时间复杂度:最优时间复杂度是序列已处于升序状态,不需要重排,此时内循环复杂度是O(1),外循环是O(n),因此最优时间复杂度是O(n);最坏时间复杂度,i=1时内循环1次,i=2时内循环2次,直到i=n,总循环次数为1+2+3+....+n=n²/2+n/2,时间复杂度为O(n^2)。
插入排序的缺点:插入排序只会交换相邻元素,元素只能从数组的一端一点一点移动到另一端,对于大规模乱序数据,插入排序就很慢。
2.希尔排序
插入排序对于排序已经差不多的序列,效率高,因此希尔排序是插入排序的改进版,即将整个序列按步长分成子序列,对子序列进行插入排序,在整个序列达到“基本有序”后,再通过插入排序对整个序列排序。
举例:对【6,5,4,3,2,1】排序
第一次排序: 第一次的步长为6//2=3
比较【6,3】:交换【3,5,4,6,2,1】
比较【5,2】:交换【3,2,4,6,5,1】
比较【4,1】:交换【3,2,1,6,5,4】第二次排序: 第二次的步长3//2=1
比较【3,2】:交换【2,3,1,6,5,4】
比较【3,1】:交换【2,1,3,6,5,4】
比较【2,1】:交换【1,2,3,6,5,4】
比较【3,6】:不变【1,2,3,6,5,4】
比较【6,5】:交换【1,2,3,5,6,4】
比较【3,5】:不变【1,2,3,5,6,4】
比较【6,4】:交换【1,2,3,5,4,6】
比较【5,4】:交换【1,2,3,4,5,6】
希尔排序的python代码实现:
算法思路(结合上面的例子):外外循环是以不同的步长分组进行插入排序,外循环中i从gap开始自增(因为j的取值要满足j=i-gap>0),j=i,内循环中j以步长gap自减,i的每次循环中v都与a[j-gap]比较大小来进行插入排序(每次都是只互换相邻的两个元素),当j小于gap或v大于a[j-gap]时循环结束。
def shellSort(list_):
n = len(a)
gap = n // 2
while gap > 0:
for i in range(gap, n):
v = list_[i]
j = i
while j >= gap and v < list_[j-gap]:
list_[j] = list_[j-gap]
j = j - gap
list_[j] = v
gap = gap // 2
return list_
a = [9,1,2,5,7,4,8,6,3,5]
print('原序列:',a)
a1 = shellSort(a)
print('希尔排序后:', a1)
输出结果:
原序列: [9, 1, 2, 5, 7, 4, 8, 6, 3, 5]
希尔排序后: [1, 2, 3, 4, 5, 5, 6, 7, 8, 9]
希尔排序的时间复杂度:希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小,插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比O(n^2)好一些。
3.冒泡排序
每次循环中,从一端到另一端只比较相邻元素,顺序不对就交换,每次循环下来都会得到一个最大或最小值移动到了尾或首端,重复循环直到没有需要再交换的。
与插入排序的区别:冒泡排序相邻元素交换后,不管与之前元素的顺序是不是对的都继续往下进行。
冒泡排序的python实现:
从头到尾将序列从小到大排列,将数组分成未排序(前)和已排序(后)部分,i的每次循环都能排好一个最大或最小元素,循环n-1次就是将所有元素都排完;j表示对每个未排序元素进行比较排序。
def bubbleSort(a):
n = len(a)
for i in range(n):
for j in range(0, n-1-i):
if a[j] > a[j+1]:
v = a[j+1]
a[j+1] = a[j]
a[j] = v
return a
a = [9,1,2,5,7,4,8,6,3,5]
print('原序列:',a)
a1 = bubbleSort(a)
print('冒泡排序后:', a1)
输出结果:
原序列: [9, 1, 2, 5, 7, 4, 8, 6, 3, 5]
冒泡排序后: [1, 2, 3, 4, 5, 5, 6, 7, 8, 9]
4.快速排序
选定一个基数,通过一趟排序要将原来数组分成两部分:比基数大的子序列和比基数小的子序列,然后再子序列中再分别递归按照上述方法进行,经过若干趟排序让整个序列变有序
策略:分治法,递归
快速排序的实现过程(升序):
(1) 对于序列a,选定一个基数x(一般都取序列的第一个元素),序列最左边元素定义为a[low],low从0递增,序列最右边元素定义为a[high],high从n-1递减。
(2) 第一趟排序中,从high开始,若a[high]>x,则high-1;若a[high]<x,则将a[high]赋值给a[low].
(3) 再从low开始,若a[low]<x,则low+1;若a[low]>x,则将a[low]赋值给a[high]
(4) 再从high开始重复上步骤(2)(3),直到high=low
(5) 第一趟排序后,子序列被分成两部分:比基数大的部分和比基数小的部分,在分别在每一部分重复上述步骤(1)~(4),知道最终所有子序列都排列完。
借鉴自:快速排序
快速排序python代码实现:
def quickSort(a, start, end):
if start < end:
low = start
high = end
x = a[low]
while low < high:
while low < high and a[high] >= x:
high -= 1
a[low] = a[high]
while low < high and a[low] <= x:
low +=1
a[high] = a[low]
a[low] = x
quickSort(a, start, low-1) #前半部分递归
quickSort(a, high+1, end) #后半部分递归
return a
a = [3, 4, 6, 1, 2, 5, 7, 1]
print("排序前:",a)
n = len(a)
start = 0
end = n-1
a1 = quickSort(a, start, end)
print("快速排序后:",a1)
输出结果:
排序前: [3, 4, 6, 1, 2, 5, 7, 1]
快速排序后: [1, 1, 2, 3, 4, 5, 6, 7]
时间复杂度:快速排序的最坏时间复杂度是O(n^2),比如顺序数列的快排;平均时间复杂度是O(nlogn),O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。
5.归并排序
采用分而治之的策略,有自上而下的递归和自下而上的迭代两种方法.
归并排序过程:
分:
1 我们按照序列长度的1/2将原序列分成两个小的数组。
2 把两个小数组 再按照新长度的一半把每个小数列都分成两个更小的
...直到序列被分成单个元素。
比如: 84571362
第一次 n=8 n//2=4 分成 8457 1362
第二次 n=4 n//2=2 分成 84 57 | 13 62
第三次
n=2 n//2=1 分成 8 4 5 7 1 3 6 2,都分成了单个元素
治:
3. 按照步骤2所得的最后分开的两个元素,比较大小绑定,得到48 57 13 26,
4.循环按照分的顺序对得到的子序列进行合并,得到4578 1236,再到最终结果。
注意:每次都是取两个数组中的最小值进行比较,得出最小值之后就放进新的数组中。如果最后两个数组比较时其中一个数组提前全部放进了新数组,那把剩下的一个数组直接按顺序放心新数组就行。
归并排序的python实现:
import math
def mergeSort(a):
n = len(a)
middle = math.floor(n/2) #math.floor()是向下取整
if len(a) < 2:
return a
else:
left = a[0 : middle]
right = a[middle : ]
return merge(mergeSort(left), mergeSort(right)) #递归
def merge(left,right):
'''两个子序列都不为空时,就循环比较子序列的头元素,任意一个子序列为空时就将
另一个子序列依次加到新序列中'''
new_a = []
while left and right: #两个子序列均不为空
if left[0] < right[0]:
new_a.append(left.pop(0)) #取出索引为0的数据并将其在原序列中删除
else:
new_a.append(right.pop(0))
while left:
new_a.append(left.pop(0))
while right:
new_a.append(right.pop(0))
return new_a
a = [8, 4, 5, 7, 1, 3, 6, 2]
print('原序列:', a)
new_a = mergeSort(a)
print('归并排序后的序列:', new_a)
输出结果:
原序列: [8, 4, 5, 7, 1, 3, 6, 2]
归并排序后的序列: [1, 2, 3, 4, 5, 6, 7, 8]
时间复杂度:比较稳定,复杂度为O(nlogn),但由于每次得创建新数组,所以空间复杂度较高。
6.选择排序
首先找序列最小值,如果不是在头元素就与头元素互换,作为已排序序列,在所有未排序序列中继续寻找最小值,若不是在未排序序列的头元素就将其与头元素互换,加入已排序序列,循环直到未排序序列为空。
选择排序python实现:
先假设a[0]最小,比较a[0]与其后面元素的大小,若a[0]非最小,就将a[0]与最小值互换;再比较a[1]与其后面的元素大小.....直到a[n-1]。
def selectSort(a):
n = len(a)
for i in range(n):
minnum = i
for j in range(i, n):
if a[j] < a[minnum]:
minnum = j #记录最小数的索引,因为不知道循环到哪才是真正的最小值,因此不能在这里直接互换
if minnum != i :
a[i], a[minnum] = a[minnum], a[i] #互换
return a
a = [8, 4, 5, 7, 1, 3, 6, 2]
print('原序列:', a)
new_a = selectSort(a)
print('选择排序后的序列:', new_a)
输出结果:
原序列: [8, 4, 5, 7, 1, 3, 6, 2]
选择排序后的序列: [1, 2, 3, 4, 5, 6, 7, 8]
选择排序的时间复杂度:O(n^2)
7.堆排序
关于堆(借鉴自:图解排序算法之堆排序)
堆是一种数据结构,又叫完全二叉树,分为大顶堆和小顶堆。
大顶堆:从上而下每个结点的值都大于或等于其左右子结点的值。
小顶堆:从上而下每个结点的值都小于或等于其左右子节点的值。
将这种逻辑结构映射到数组中:大顶堆[50,45,40,20,25,35,30,10,15] ;小顶堆[10,20,15,25,50,30,40,35,45],自上而下,自左向右映射。
大顶堆
该数组从逻辑结构上来讲就是一个堆结构,用公式描述堆结构的定义就是:
大顶堆:arr[i]>=arr[2i+1] && arr[i]>=arr[2i+2]
小顶堆:arr[i]<=arr[2i+1] && arr[i]<=arr[2i+2]
关于堆排序
堆排序就是首先将序列按照堆的定义重复构造完全二叉树,取堆顶的最大或最小值与数组未排序元素进行互换,循环直到有序。比如构造升序序列时,将数组构造大顶堆,这样堆顶的元素即最大值,将此最大值与arr[n-1]互换;再重新构造大顶堆,将堆顶元素与arr[n-2]互换....直到a[0]完成排序。 (一般升序用大顶堆,降序用小顶堆)
堆排序python实现:
首先明确,在树结构中,最后一个叶子节点的索引值是n-1,最后一个非叶子节点的索引值是[(n-1)-1]/2=n/2-1。通过对堆排序函数递归调用来实现堆排序。
import math
def heapAdjust(arr, i):
'堆调整'
left = 2*i + 1
right = 2*i + 2
largest = i
if left < length and arr[left] > arr[largest]:
largest = left
if right < length and arr[right] > arr[largest]:
largest = right
if largest != i:
arr[i], arr[largest] = arr[largest], arr[i] #交换
#一次交换调整后,还需要比较调整后的元素位置和其下面的节点是否符合规则
heapAdjust(arr, largest)
def maxHeap(arr):
#建立初始大顶堆
for i in range(math.floor(length/2), -1, -1):
heapAdjust(arr, i)
def heapSort(arr):
#堆排序交换元素
global length
length = len(arr)
maxHeap(arr)
for i in range(length-1, 0, -1):
arr[0], arr[i] = arr[i], arr[0]
length -= 1
#构造好初始大顶堆后,后面排序再调整的话从顶向下调整就行了
heapAdjust(arr, 0)
return arr
arr = [8, 4, 5, 7, 1, 3, 6, 2]
print("排序前:", arr)
arr1 = heapSort(arr)
print("排序后:", arr1)
堆排序的时间复杂度:O(nlogn)
8.计数排序
(后面有时间再整理)
9.桶排序
10.基数排序
标签:arr,复杂度,元素,算法,序列,排序,初等,插入排序 来源: https://blog.csdn.net/m0_55519533/article/details/119826114