冒泡排序
1、冒泡排序的思路
-
比较相邻元素:从数组的第一个元素开始,依次比较相邻的两个元素。
-
交换位置:如果前面的元素比后面的元素大(升序排序),则交换它们的位置;如果前面的元素比后面的元素小(降序排序),则不交换。
-
一轮比较:每完成一轮比较,都会将当前未排序部分中的最大(或最小)元素移动到正确的位置。
-
重复步骤:重复上述步骤,每次比较未排序部分的元素,直到整个数组排序完成。
-
冒泡排序的核心在于通过不断交换相邻的元素,并向数组尾部推进最大的元素,从而达到排序的目的。尽管冒泡排序的时间复杂度为O(n^2),并不是最高效的排序算法,但它易于理解和实现,适用于简单的排序任务或者教学目的。
2、冒泡排序的图解
[6,2,1,4,7,3]
6 | 2 | 1 | 4 | 7 | 3 |
2 | 6 | 1 | 4 | 7 | 3 |
2 | 1 | 6 | 4 | 7 | 3 |
2 | 1 | 4 | 6 | 7 | 3 |
2 | 1 | 4 | 6 | 7 | 3 |
2 | 1 | 4 | 6 | 3 | 7 |
依次类推... | |||||
1 | 2 | 4 | 6 | 3 | 7 |
1 | 2 | 4 | 6 | 3 | 7 |
3、冒泡排序的实现
list=[6,2,1,4,7,3,5]
for i in range(len(list)):
for j in range(len(list)-i):
if(list[i]<list[j]):
list[i],list[j]=list[j],list[i]
print(list)
思路再分析:
-
第一次找出最大的数据放在最后, 我们需要两个两个数据项进行比较, 那么这个应该是一个循环操作.
-
第二次将次最大的数据找到放在倒数第二个位置, 也是两个比较, 只是不要和最后一个比较(少了一次), 但是前面的两个两个比较也是一个循环操作.
-
第三次...第四次...
选择排序
选择排序改进了冒泡排序
1、选择排序的思路
-
找到最小元素:首先在未排序的部分找到最小(或最大)的元素。
-
交换位置:将找到的最小元素与未排序部分的第一个元素交换位置。
-
标记已排序部分:将交换后的元素视为已排序的一部分,然后继续在剩余未排序的部分中寻找下一个最小元素。
-
重复步骤:重复以上步骤,每次找到剩余未排序部分的最小元素,并将其加入已排序部分,直到所有元素都排序完成。
2、选择排序的实现
list=[6,7,2,1,4,3] for j in range(len(list)-1): index=j for i in range(j+1,len(list)): # print(i) if list[index]<list[i]: index=i#每次判断后直接把下标交换了 list[index],list[j]=list[j],list[index]#循环完毕后 一次性交换数据 print(list)
选择排序的特点是每次找到未排序部分的最小元素并交换,相较于冒泡排序,它减少了元素比较的次数,但仍然具有O(n^2)的时间复杂度。尽管如此,选择排序在实现上比冒泡排序稍微高效一些,因为它减少了元素交换的次数。
总结来说,选择排序的思路就是不断选择未排序部分的最小元素,并将其放到已排序部分的末尾,直到整个数组排序完成。
插入排序
1、插入排序思路分析
-
分成已排序和未排序两部分:初始时,将数组分为已排序部分和未排序部分。通常初始时,已排序部分只包含数组的第一个元素,而剩余部分是未排序的。
-
逐步扩大已排序部分:从未排序部分取出第一个元素,将它插入到已排序部分的正确位置。在已排序部分中,从右向左比较已排序的元素,找到合适的位置插入该元素,使得插入后仍然保持有序。
-
重复步骤:重复上述步骤,每次将未排序部分的第一个元素插入到已排序部分的正确位置,直到所有元素都被排序。
-
结束条件:当未排序部分没有元素时,排序完成。
2、插入排序的实现
def insertion_sort(arr):
length = len(arr)
for i in range(1, length):
j = i
temp = arr[i]
while j > 0 and arr[j-1] > temp: #谁比temp大谁就往后移动一个位置 一直比到一个数据
arr[j] = arr[j-1]
j -= 1
arr[j] = temp #谁是最后一个移动位置的 把它的位置设置为temp
list=[6,2,1,4,7,3,5]
insertion_sort(list)
print(list)
归并排序
1、设计思路
基本思想与过程:先递归的分解数列,再合并数列(分治思想的典型应用)
-
(1)将一个数组拆成A、B两个小组,两个小组继续拆,直到每个小组只有一个元素为止。
(2)按照拆分过程逐步合并小组,由于各小组初始只有一个元素,可以看做小组内部是有序的,合并小组可以被看做是合并两个有序数组的过程。
(3)对左右两个小数列重复第二步,直至各区间只有1个数。
下面对数组【42,20,17,13,28,14,23,15】进行归并排序,模拟排序过程如下:
第一步:拆分数组,一共需要拆分三次;
第一次拆成【42,20,17,13】,【28,14,23,15】,
第二次拆成【42,20】,【17,13】,【28,14】,【23,15】,、
第三次拆成【42】,【20】,【17】,【13】,【28】,【14】,【23】,【15】;
第二步:逐步归并数组,采用合并两个有序数组的方法
第一次归并为【20,42】,【13,17】,【14,28】,【15,23】
第二次归并为【13,17,20,42】,【14,15,23,28】,
第三次归并为【13, 14, 15, 17, 20, 23, 28, 42】
2、代码实现
#辅助函数:合并两个列表
#left和right中的最小数据拿出来比较 谁小 谁就添加进去 其中一个没有数据了就把剩下的全部加进去
def merge(left, right):
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] < right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result += left[i:]
result += right[j:]
return result
#归并排序
def merge_sort(arr):
length = len(arr)
if length > 1:
index = length // 2
left = arr[:index]
right = arr[index:]
return merge(merge_sort(left), merge_sort(right))
else:
return arr
list=[42,20,17,13,28,14,23,15]
newlist=merge_sort(list)
print(newlist)
快速排序
1、设计思路
-
选择基准:从数组中选择一个基准元素。通常可以选择第一个元素、最后一个元素或者中间元素作为基准。
-
分区操作:重新排序数组,将比基准元素小的所有元素放在基准元素的前面,将比基准元素大的所有元素放在基准元素的后面。在这个分区结束之后,该基准元素就处于最终排好序的位置。
-
递归地对左右两部分排序:递归地调用上述分区操作,对左右两个子数组分别进行排序。
-
递归结束条件:当左右各部分只有一个元素时,排序完成。
具体步骤如下:
-
选择基准:从数组中选取一个基准元素,通常选择数组的第一个元素。
-
分区操作:设定两个指针,左指针指向数组的起始位置,右指针指向数组的末尾。移动这两个指针,使得左指针左侧的元素都小于等于基准元素,右指针右侧的元素都大于等于基准元素。交换左右指针所指的元素,直到左右指针相遇。
-
基准元素归位:将基准元素与左右指针相遇的位置元素交换,这样基准元素就位于最终的位置。
-
递归排序:递归地对基准元素左侧的子数组和右侧的子数组进行分区操作,直到所有的子数组只有一个元素。
2、代码实现
def quick_sort(arr):
if len(arr) <= 1:
return arr
pivot = arr[len(arr) // 2]
left = [x for x in arr if x < pivot]
middle = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]
return quick_sort(left) + middle + quick_sort(right)
list=[13,81,92,43,65,31,57,26,75,0]
newlist=quick_sort(list)
print(newlist)
快速排序的时间复杂度平均情况下为O(n log n),最坏情况下为O(n^2),取决于选取的基准元素以及数组的初始状态。它是原地排序算法,不需要额外的空间用于存储临时数组,但是递归调用的深度最坏情况下可能达到O(n),因此在处理大规模数据时需要注意栈溢出的问题。
总体来说,快速排序通过不断地将数组分区并递归地对分区进行排序,直到整个数组有序,是一种高效且常用的排序算法。
顺序搜索
从数据结构的第一个元素开始逐个检查,直到找到目标值或遍历完整个数据集。适用于数组和链表等基本数据结构。
list=[10,203,5,4,3,54,65] #遍历 把数据容器中的所有数据取出来使用 def fn(list,el): for item in list: if item==el: return True return False re=fn(list,50) print(re)
二分搜索
针对有序数组进行搜索,通过不断将搜索范围减半来提高查找效率。每次都将搜索区间划分为两部分,并基于中间元素与目标值的比较决定是在左半边还是右半边继续搜索。
-
确定搜索范围:首先确定整个数组的搜索范围,通常是从数组的起始位置到结束位置。
-
计算中间元素:计算中间元素的索引,即将搜索范围的中间位置标记为中间索引。
-
比较并缩小搜索范围:将目标元素与中间元素进行比较。
- 如果目标元素等于中间元素,则找到目标,返回中间索引。
- 如果目标元素小于中间元素,则说明目标元素可能在左半部分,更新搜索范围为左半部分。
- 如果目标元素大于中间元素,则说明目标元素可能在右半部分,更新搜索范围为右半部分。
-
重复直到找到目标或搜索范围为空:重复上述步骤,直到找到目标元素或者搜索范围为空(即起始位置大于结束位置)为止。
def binary_search(arr,targeet): arr.sort(key=lambda x:x) print(arr) left=0 right=len(arr) while left<right: index = (left + right) // 2 #6 10 if arr[index]==targeet: return True elif arr[index]<targeet: left=index+1#7 else: right=index-1 return False
二分搜索的时间复杂度为O(log n),因为每次搜索操作都将搜索范围缩小一半。这使得它比线性搜索更加高效,特别适用于已排序的数组或者有序列表的查找操作。
需要注意的是,二分搜索要求数据结构具备随机访问的特性,即可以通过索引快速访问任意位置的元素。这种搜索算法在实际应用中非常常见,例如在数据库索引、字典查找等场景中广泛使用。
大O表示法
大O表示法(Big O notation)是一种用于描述算法性能与输入规模之间的关系的标准数学符号,它提供了算法在最坏情况下的时间复杂度和空间复杂度的上界估计。在计算机科学中,算法的时间复杂度是指执行算法所需要的计算工作量随着数据的大小n的增长而增长的速度。空间复杂度则是指算法在运行过程中临时占用存储空间的增长速度。
例:假设列表有n个元素,简单查找需要查找每个元素,因此需要执行n次操作。使用大O表示法记做:O(n),如果用二分法查找,则需要执行 log 2n 次操作。使用大O表示法记做:O(log 2n)
时间复杂度
时间复杂度衡量的是执行算法所需的基本操作次数,不考虑具体操作的时间开销,只关注操作次数随输入规模n增加的变化趋势。例如:
-
常数时间复杂度 O(1):算法的运行时间不随输入数据量增大而增大,如访问数组中的一个确定下标的元素。
-
对数时间复杂度 O(log n):算法的时间消耗以对数函数增长,常见于二分查找等算法。
-
线性时间复杂度 O(n):算法所需时间与输入规模成正比,比如遍历一个数组。
-
线性对数时间复杂度 O(n log n):常见于快速排序、归并排序等高效排序算法。
-
二次时间复杂度 O(n²):如冒泡排序、选择排序等简单排序算法,时间消耗与输入规模的平方成正比。
-
立方时间复杂度 O(n³):如果算法中有三层嵌套循环,每层循环都与n有关,则可能达到此复杂度。
-
指数时间复杂度 O(2^n):如完全枚举所有可能性的算法,在问题规模增大时增长极为迅速。
空间复杂度
空间复杂度衡量算法在运行过程中额外需要的存储空间大小。同样,它也采用大O表示法来描述空间需求与n的关系。
-
常数空间复杂度 O(1):算法所需的额外空间与输入数据量大小无关,始终保持不变。
-
线性空间复杂度 O(n):额外空间的需求随着输入规模线性增长,例如复制一份输入数组。
-
其他复杂度:根据算法的具体实现和所需额外存储的数据结构,可以有不同的空间复杂度。
大O表示法并不是精确的执行时间,而是用来表示算法在最坏情况下的增长速率。例如,一个算法的时间复杂度为O(n),意味着当输入大小为n时,执行时间以线性方式增长。在实际应用中,大O表示法帮助我们评估算法的效率和可伸缩性,选择最适合特定问题的算法。
总结来说,大O表示法是一种标准化的量级评估方法,帮助我们比较和分析不同算法在处理大规模数据时的表现。