排序
- 冒泡排序、插入排序、选择排序这三种排序算法,它们的时间复杂度都是 O(n2),比较高,适合小规模数据的排序。今天,我讲两种时间复杂度为 O(nlogn) 的排序算法,归并排序和快速排序。这两种排序算法适合大规模的数据排序
- 归并排序和快速排序都用到了分治思想,非常巧妙。我们可以借鉴这个思想,来解决非排序的问题,比如:如何在 O(n) 的时间复杂度内查找一个无序数组中的第 K 大元素?
- 分治算法一般都是用递归来实现的。分治是一种解决问题的处理思想,递归是一种编程技巧,这两者并不冲突。
1. 冒泡排序
O(n^2)
def BubbleSort(alist):
l=len(alist)-1
sort=1 #用来表示一轮冒泡排序中是否有排过序,如果没有排过序,说明排序已经完成,不需要再进行下去
while l>0 and sort==1: #最多需要排序n-1次,加上sort判断可能提前结束排序
sort=0
for i in range(l):
if alist[i]>alist[i+1]:
alist[i],alist[i+1]=alist[i+1],alist[i]
sort=1
l-=1
print(alist)
2. 选择排序
- 选择排序改进了冒泡排序,每次遍历列表只做一次交换。为了做到这一点,一个选择排序在他遍历时寻找最大的值,并在完成遍历后,将其放置在正确的位置。与冒泡排序一样,在第一次遍历后,最大的项在正确的地方。 第二遍后,下一个最大的就位。遍历 n-1 次排序 n 个项,因为最终项必须在第(n-1)次遍历之后。
O(n^2),但交换数量减少,速度加快。
def selectionSort(alist):
l=len(alist)-1
while l>0:
positionmax=0
for i in range(l):
if alist[positionmax]<alist[i]:
positionmax=i
alist[positionmax],alist[l]=alist[l],alist[positionmax]
l-=1
print(alist)
3. 插入排序
- 插入排序,尽管仍然是 O(n^2),工作方式略有不同。它始终在列表的较低位置维护一个排序的子列表。然后将每个新项 “插入” 回先前的子列表,使得排序的子列表称为较大的一个项。展示了插入排序过程。 阴影项表示算法进行每次遍历时的有序子列表。
def insertionSort(alist):
l=len(alist)
for i in range(1,l):
insertvalue=alist[i]
position=i-1
while position>=0 and insertvalue<alist[position]:
alist[position+1]=alist[position]
position-=1
alist[position+1]=insertvalue
4. 快速排序
- 快速排序首先选择一个值,该值称为 枢轴值。虽然有很多不同的方法来选择枢轴值,我们将使用列表中的第一项。枢轴值的作用是帮助拆分列表。枢轴值属于最终排序列表(通常称为拆分点)的实际位置,将用于将列表划分为快速排序的后续调用。
在这里插入代码片
5. 归并排序
归并排序是一种 O(nlogn)算法。
def mergeSort(alist):
print("Splitting ",alist)
if len(alist)>1:
mid = len(alist)//2
lefthalf = alist[:mid]
righthalf = alist[mid:]
mergeSort(lefthalf)
mergeSort(righthalf)
i=0
j=0
k=0
while i < len(lefthalf) and j < len(righthalf):
if lefthalf[i] < righthalf[j]:
alist[k]=lefthalf[i]
i=i+1
else:
alist[k]=righthalf[j]
j=j+1
k=k+1
while i < len(lefthalf):
alist[k]=lefthalf[i]
i=i+1
k=k+1
while j < len(righthalf):
alist[k]=righthalf[j]
j=j+1
k=k+1
print("Merging ",alist)
6. 编程实现 O(n) 时间复杂度内找到一组数据的第 K 大元素
- 平均复杂度,实际上如果每次刚好二分,第一次取flag比较次数是n,第二次是n/2,依次下去是n/4,n/8…n/2,n(1+1/2+1/4+1/8+…1/2ⁿ)=2n,复杂度自然就是O(n)了
参考:https://blog.csdn.net/u011519550/article/details/89417311
#给定一个无序列表,求出第K大的元素,要求复杂度O(n)
def find_k(test_list,k):
flag=test_list[0]
test_list.pop(0)
l_list=[i for i in test_list if i < flag]
r_list=[i for i in test_list if i >= flag]
#结果递归的基线条件
if len(r_list)==k-1:
return flag
elif len(r_list)>k-1:
return find_k(r_list,k)
else:
#因为test_list.pop(0)让test_list少了一个元素,所以下面需要+1
gap=len(test_list)-len(l_list)+1
k=k-gap
return find_k(l_list,k)
if __name__ == '__main__':
test_list = [5, 4, 3, 2, 1,10,20,100]
res=find_k(test_list,1)
print(res)
练习:
滑动窗口最大值
中文版:https://leetcode-cn.com/problems/sliding-window-maximum/
英文版:https://leetcode.com/problems/sliding-window-maximum/
- 给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口 k 内的数字。滑动窗口每次只向右移动一位。
返回滑动窗口最大值。
示例:
输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
输出: [3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
注意:
你可以假设 k 总是有效的,1 ≤ k ≤ 输入数组的大小,且输入数组不为空。
参考:https://www.jianshu.com/p/a37a8e1e5a23
def maxSlidingWindow(self, nums, k):
from collections import deque
"""
:type nums: List[int]
:type k: int
:rtype: List[int]
"""
length = len(nums)
res = []
q= deque()
for idx ,v in enumerate(nums):
#检查如果左边界超出窗口就弹出队列头
if len(q)>0 and q[0]<=idx -k:
t =q.popleft()
#循环检查队尾是否大于当前元素
while len(q)>0 and nums[q[-1]]< v:
q.pop()
q.append(idx)
if (idx>=k-1):
res.append(nums[q[0]])
return res
二分查找
编程
- 有序列表对于我们的比较是很有用的。在顺序查找中,当我们与第一个项进行比较时,如果第一个项不是我们要查找的,则最多还有 n-1 个项目。 二分查找从中间项开始,而不是按顺序查找列表。 如果该项是我们正在寻找的项,我们就完成了查找。 如果它不是,我们可以使用列表的有序性质来消除剩余项的一半。如果我们正在查找的项大于中间项,就可以消除中间项以及比中间项小的一半元素。如果该项在列表中,肯定在大的那半部分。
- 然后我们可以用大的半部分重复这个过程。从中间项开始,将其与我们正在寻找的内容进行比较。再次,我们找到元素或将列表分成两半,消除可能的搜索空间的另一部分。
- 二分查找是 O(logn),顺序查找是O(n)
1. 实现一个有序数组的二分查找算法
def binarySearch(alist, item):
first = 0
last = len(alist)-1
found = False
while first<=last and not found:
midpoint = (first + last)//2
if alist[midpoint] == item:
found = True
else:
if item < alist[midpoint]:
last = midpoint-1
else:
first = midpoint+1
return found
递归调用二分查找函数:
def binarySearch(alist, item):
if len(alist) == 0:
return False
else:
midpoint = len(alist)//2
if alist[midpoint]==item:
return True
else:
if item<alist[midpoint]:
return binarySearch(alist[:midpoint],item)
else:
return binarySearch(alist[midpoint+1:],item)
2. 实现模糊二分查找算法(比如大于等于给定值的第一个元素)
def fuzzybinarySearch(alist, item):
first = 0
last = len(alist)-1
found = False
while first<=last and not found:
midpoint = (first + last)//2
if alist[midpoint] == item or (alist[midpoint]>item and alist[midpoint-1] <item):
found = True
position=midpoint
elif alist[first] > item:
found = True
position=first
else:
if item < alist[midpoint]:
last = midpoint-1
else:
first = midpoint+1
return position
def fuzzybinarySearch(alist, item):
start = 0
end = len(alist)-1
mid = (start + end +1) // 2 #向上取整
while not end == mid:
if alist[mid] > item:
end = mid
mid = (start + end +1) // 2
elif alist[mid] < item:
start = mid
mid = (start + end +1) // 2
else:
return mid
return mid
练习:
Sqrt(x) (x 的平方根)
英文版:https://leetcode.com/problems/sqrtx/
中文版:https://leetcode-cn.com/problems/sqrtx/
实现 int sqrt(int x) 函数。
计算并返回 x 的平方根,其中 x 是非负整数。
由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。
示例 1:
输入: 4
输出: 2
示例 2:
输入: 8
输出: 2
说明: 8 的平方根是 2.82842…,
由于返回类型是整数,小数部分将被舍去。
- 二分查找
根据 ( n − 1 ) 2 > = 0 − > n < = ( n + 2 ) / 2 (\sqrt{n} \quad-1)^2>=0\quad->\sqrt{n}<=(n+2)/2 (n−1)2>=0−>n<=(n+2)/2
n的平方根值有一个上限,得到我们需要查找的范围。
算法与模糊二分查找算法基本相同
class Solution:
def mySqrt(self, x):
if x <= 1:
return x
start = 1
end = x//2 +1
mid = (start + end) // 2
while not start == mid:
if mid ** 2 > x:
end = mid
mid = (start + end) // 2
elif mid ** 2 < x:
start = mid
mid = (start + end) // 2
else:
return mid
return mid
- 牛顿迭代法
class Solution:
def mySqrt(self, x):
"""
:type x: int
:rtype: int
"""
if x <= 1:
return x
r = x
while r > x / r:
r = (r + x / r) // 2
return int(r)