前言
说起排序啊,我心里想到的第一个例子就是给定三个数a,b,c按照从小到大的顺序排序输出,这应该算是我们接触编程遇到的最简单的排序问题了叭。不知道大家当时是怎么解决这个问题的?别告诉我你直接sort()哈,其实我当初遇到这个问题的时候是想了一会儿的,包括当时遇到比较三个数大小的问题时,在我们眼里比较三个数的大小或者排序应该是一眼睛就看穿的问题了,但是放进程序语言里可能没有你想象的那么简单。
用C语言写的话,我们之前像这样一定试过,中间的三个if语句写的真是脑壳大。
# include<stdio.h>
int main(){
int a,b,c,temp;
//读入三个数
scanf("%d %d %d",&a,&b,&c);
//三个if 后续有列表解析过程
if(a>b) temp=a,a=b,b=temp;
if(b>c) temp=b,b=c,c=temp;
if(a>b) temp=a,a=b,b=temp;
//打印排序后的三个数
printf("%d %d %d",a,b,c);
return 0;
或许三个数我们还能像这样的完成,要是30个300个数呢?if语句肯定是不行的,这时候就到了我们排序算法发挥他真正实力的时候了!
排序算法
冒泡排序
相信大多数的同学包括我自己接触到的第一个排序算法应该就是冒泡排序。
冒泡排序(英语:Bubble Sort)是一种简单的排序算法。它重复地遍历要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。遍历数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
上面是冒泡排序的一个GIF演示图:从第一个元素开始,相邻的两个元素比较大小,然后小的元素放在左边,大的元素就往右边排,这样一趟的话就能找出序列里最大的元素,然后再走n-1趟,倒数第二大的,倒数第三大的…就依次被找出来,直到这个序列有序为止。概括来说:
冒泡排序算法的运作如下:
- 比较相邻的元素。如果第一个比第二个大(升序),就交换他们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
def buble_sort(alist):
"""冒泡排序"""
n = len(alist)
for j in range(n-1):
# 要走多少趟
count = 0
for i in range(0, n-1-j):
# 从头走到尾要走多少次
if alist[i] > alist[i+1]:
alist[i], alist[i+1] = alist[i+1], alist[i]
count += 1
if count == 0:
return
这个冒泡排序是经过改进的,在于我们解决了一个已经排好序的列表的冒泡排序问题,降低了该算法的时间复杂度。
选择排序
选择排序(Selection
sort)是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对n个元素的表进行排序总共进行至多n-1次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。
def select_sort(alist):
"""选择排序"""
n = len(alist)
for j in range(0, n-1):
min_index = j
for i in range(j+1, n):
if alist[min_index] > alist[i]:
min_index = i
alist[j], alist[min_index] = alist[min_index], alist[j]
这些代码是个啥意思呢?先看里面的那个循环,假设我第一个数是最小的,然后我依次遍历后面的n-1个数,如果我找到了一个比这个数更小的数,我把最小值的下标记住,然后直到找到最小的,第一层遍历结束,再把这个数与第一个数交换,那么最小的数就排在第一个了。按照这个规律,我再遍历n-2遍直到整个序列有序为止。
插入排序
插入排序(英语:Insertion Sort)是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
这个图片的演示就很直观了:从第二个元素开始,如果第二个元素小于第一个元素,我就把第二个元素放在最左边,然后是第三个元素,如果小于第二个,再与第一个比较,如果小于第一个,那第三个元素就放在第一个元素的位置,以此类推。
def insert_sort(alist):
"""插入排序"""
n = len(alist)
# 从右边的无序序列中取出多少个元素执行这样的过程
for j in range(1, n):
# j = [1, 2, 3, n-1]
# i 代表内层循环起始值
i = j
# 执行从右边的无序序列中取出第一个元素,即i位置的元素,然后将其插入到前面的正确位置中
while i > 0:
if alist[i] < alist[i-1]:
alist[i], alist[i-1] = alist[i-1], alist[i]
i -= 1
else:
break
先看while循环,如果第i个元素小于他前面的一个元素,我们就将两个元素交换位置,也就是较小的元素前移一位,然后这个元素再与他前面的元素比较,直到有个元素比他小了,循环结束,这个元素的位置也就确定了。再看外层for循环,i表示是第几个元素,我有几个元素就要执行几次,每次找到一个元素的正确位置,知道最后序列有序为止。
希尔排序
希尔排序(Shell Sort)是插入排序的一种。也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因DL.Shell于1959年提出而得名。 希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
希尔排序的基本思想是:将数组列在一个表中并对列分别进行插入排序,重复这过程,不过每次用更长的列(步长更长了,列数更少了)来进行。最后整个表就只有一列了。将数组转换至表是为了更好地理解这算法,算法本身还是使用数组进行排序。
def shell_sort(alist):
"""希尔排序"""
# n=9
n = len(alist)
# gap =4
gap = n // 2
# i = gap
# for i in range(gap, n):
# # i = [gap, gap+1, gap+2, gap+3... n-1]
# while:
# if alist[i] < alist[i-gap]:
# alist[i], alist[i-gap] = alist[i-gap], alist[i]
# gap变化到0之前,插入算法执行的次数
while gap > 0:
# 插入算法,与普通的插入算法的区别就是gap步长
for j in range(gap, n):
# j = [gap, gap+1, gap+2, gap+3, ..., n-1]
i = j
while i > 0:
if alist[i] < alist[i-gap]:
alist[i], alist[i-gap] = alist[i-gap], alist[i]
i -= gap
else:
break
# 缩短gap步长
gap //= 2
希尔排序是一种改进的特殊的插入排序算法,所以里面很多都是插入排序里面的步骤,只是每次比较的元素位置不同,希尔排序每次比较步长为指定数字的两个元素,然而插入排序步长为1。
归并排序
归并排序是采用分治法的一个非常典型的应用。归并排序的思想就是先递归分解数组,再合并数组。
将数组分解最小之后,然后合并两个有序数组,基本思路是比较两个数组的最前面的数,谁小就先取谁,取了后相应的指针就往后移一位。然后再比较,直至一个数组为空,最后把另一个数组的剩余部分复制过来即可。
def merge_sort(alist):
"""归并排序"""
n = len(alist)
if n <= 1:
return alist
mid = n//2
# left 采用归并排序后形成的有序的新的列表
left_li = merge_sort(alist[:mid])
# right 采用归并排序后形成的有序的新的列表
right_li = merge_sort(alist[mid:])
# 将两个有序的子序列合并为一个新的整体
# merge(left, right)
left_pointer, right_pointer = 0, 0
result = []
while left_pointer < len(left_li) and right_pointer < len(right_li):
if left_li[left_pointer] <= right_li[right_pointer]:
result.append(left_li[left_pointer])
left_pointer += 1
else:
result.append(right_li[right_pointer])
right_pointer += 1
result += left_li[left_pointer:]
result += right_li[right_pointer:]
return result
归并排序是一个递归的过程,为什么这么说呢?首先我们将这个序列2分,然后再把子序列 二分,直到每一个子序列都只含有两个元素,我们将这两个元素排序(这应该是很简单的),然后在将每两个子序列排序合并成一个更大的序列,两个更大的序列再排序合并成更更大的序列。中间的排序过程是怎样的呢?首先每一个要合并的子序列都有一个指向第一个元素的游标,两个子序列游标所指的数中找出最小的数,该数所指的游标+1,再比较,找出小的数跟原来那个数构成一个序列,以此类推,知道序列有序为止。
快速排序
快速排序(英语:Quicksort),又称划分交换排序(partition-exchange sort),通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
步骤为:
- 从数列中挑出一个元素,称为"基准"(pivot)。
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
def quick_sort(alist, first, last):
"""快速排序"""
if first >= last:
return
mid_value = alist[first]
low = first
high = last
while low < high:
# high 左移
while low < high and alist[high] >= mid_value:
high -= 1
alist[low] = alist[high]
while low <high and alist[low] < mid_value:
low += 1
alist[high] = alist[low]
# 从循环退出时,low==high
alist[low] = mid_value
# 对low左边的列表执行快速排序
quick_sort(alist, first, low-1)
# 对low右边的列表排序
quick_sort(alist, low+1, last)
快排跟归并排序有那么一点小像,都用到了递归的方法,并且都有将序列分组来处理。他是这样分组的:首先将第一个元素拿出来,我们将low游标指向第二个元素,high游标指向最后一个元素,每次low+1,high-1,然后把大于第一个元素的数字放在第一个元素的左边,大于他的放在右边,然后左右就分别有一个序列,每个子序列用同样的方法,直到整个序列有序为止。
Python中sort()函数的本质
当然我在这之前是不知道的,经过一番查询,我了解到Python中的sort()函数是归并排序和插入排序的结合版本。
还可以有这种骚操作?那我要是把所有的排序算法结合一遍我不是要吊炸天?(当然也只是想想)毕竟是人人都喜欢用的sort函数嘛,还是要整的快一点,牛批一点的,不然就失去了他作为一个内置函数的意义了。
排序算法的时间复杂度
搜索算法
搜索是在一个项目集合中找到一个特定项目的算法过程。搜索通常的答案是真的或假的,因为该项目是否存在。 搜索的几种常见方法:顺序查找、二分法查找、二叉树查找、哈希查找
二分查找
二分查找又称折半查找,优点是比较次数少,查找速度快,平均性能好;其缺点是要求待查表为有序表,且插入删除困难。因此,折半查找方法适用于不经常变动而查找频繁的有序列表。首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。
非递归实现:
def binary_search_2(alist, item):
"""二分查找, 非递归"""
n = len(alist)
first = 0
last = n-1
while first <= last:
mid = (first + last)//2
if alist[mid] == item:
return True
elif item < alist[mid]:
last = mid - 1
else:
first = mid + 1
return False
递归实现:
def binary_search(alist, item):
"""二分查找,递归"""
n = len(alist)
if n > 0:
mid = n//2
if alist[mid] == item:
return True
elif item < alist[mid]:
return binary_search(alist[:mid], item)
else:
return binary_search(alist[mid+1:], item)
return False
二分查找的最优时间复杂度:O(1),最坏时间复杂度:O(logn)。
后记
部分图片代码源于我学习的资料
还是那句话:天生我才必有用。虽然sort函数很好用,用着也方便,但是这并不妨碍我们去学习各种各样的排序算法,当我们真正的理解了当中的道道,等到实际使用的时候正确的去选择该用的算法,或许这比你一味的调用sort函数要好吧!加油!!!