-
Searching
这部分包括了无序列表下的search 和有序列表下的search
https://blog.csdn.net/oqqSSH/article/details/78442760
1.无序列表下的search
在无序的列表下只能够遍历搜索了,因为数据之间没有任何结构,代码如下:
1 def sequentialSearch(alist, item):
2 pos = 0
3 found = False
4
5 while pos < len(alist) and not found:
6 if alist[pos] == item:
7 found = True
8 else:
9 pos = pos+1
10
11 return found
12
13 testlist = [1, 2, 32, 8, 17, 19, 42, 13, 0]
14 print(sequentialSearch(testlist, 3))
15 print(sequentialSearch(testlist, 13))
其中需要寻找的值在不同的位置时需要的时间代价:
2. 有序列表(假定列表是升序排列)下的search
2.1 采用从头至尾的遍历寻找方案
假设我们需要在上述的列表中寻找数据50,则有序的寻找具有优势,在寻找到54时就可以结束算法,就证明在列表中已经不存在了该数据。 相比于无序下的search体现了一些优势,算法如下:
1 def orderedSequentialSearch(alist, item):
2 pos = 0
3 found = False
4 stop = False
5 while pos < len(alist) and not found and not stop:
6 if alist[pos] == item:
7 found = True
8 else:
9 if alist[pos] > item:
10 stop = True
11 else:
12 pos = pos+1
13
14 return found
15
16 testlist = [0, 1, 2, 8, 13, 17, 19, 32, 42,]
17 print(orderedSequentialSearch(testlist, 3))
18 print(orderedSequentialSearch(testlist, 13))
时间代价为:
2.2 二叉搜索(Binary Search)
核心思想是对有序列表在中间进行分开,并与中间值进行比较,根据中间值来确定要丢弃的部分和保留的部分。如要在下面的列表中寻找出数据54,进行了3次的分叉即可得到结果:
算法如下:
1 def binarySearch(alist, item):
2 first = 0
3 last = len(alist)-1
4 found = False
5
6 while first<=last and not found:
7 midpoint = (first + last)//2
8 if alist[midpoint] == item:
9 found = True
10 else:
11 if item < alist[midpoint]:
12 last = midpoint-1
13 else:
14 first = midpoint+1
15
16 return found
17
当然这显然也是可以通过递归来实现,base case 就是在最后的分叉中没有了数据或者找到了数据,算法如下:
1 def binarySearch(alist, item):
2 if len(alist) == 0:
3 return False
4 else:
5 midpoint = len(alist)//2
6 if alist[midpoint]==item:
7 return True
8 else:
9 if item<alist[midpoint]:
10 return binarySearch(alist[:midpoint],item)
11 else:
12 return binarySearch(alist[midpoint+1:],item)
13
14 testlist = [0, 1, 2, 8, 13, 17, 19, 32, 42,]
15 print(binarySearch(testlist, 3))
16 print(binarySearch(testlist, 13))
时间代价:对于二叉搜索而言,因为一直以二叉的方式进行搜索,所有为O(logn)。而在递归算法中,使用到了slice 的操作,而这个操作需要的时间代价是O(k),但是可以通过其他操作来取代切片的操作,所以可以忽略掉。
3 .哈希(Hashing)
创造一个抽象的数据结构来实现时间代价为O(1)的搜索,即创造一个hash table 来记录下列表,而进行查找时只需要查找这个hash table即可。一个空的hash table 如下,hash table是具有顺序的:
列表中的每一个元素都能够通过自己设计的hash function来对应于hash table中的一个元素,一个简单通用的函数是使用取余数来确定hash元素,其中的除数是hash table的大小,如一个列表对应的hash值如下:
放入到hash table中为:
但是使用简单的取余操作会带来相同的hash值,会造成冲突(collision),如列表中再加入元素44,则带来冲突,所以需要设计合理的hash function 和设计collision resolution来解决此问题。
3.1 Hash Function
对于数字可以采用折叠法(folding method)和二次中值法(mid-square method)
folding method:假设列表中的一个元素为电话号码436-555-4601,我们把它分成两个元素一组(43,65,55,46,01),然后进行相加43+65+55+46+01=219,再进行取余得到hash值,219%11=10
mid-square method:假设列表中的一个元素为44,对其进行平方得到1936,取其中的93来进行取余,得到hash值93%11=5
对于字符串来算,可以把字符串拆成单个字符,对字符寻找ASCII表对应的数值,再进行上述对数字的函数操作,如:cat
ord(‘c’)=99,ord(‘a’)=97,ord('t')=116,使用folding method得到99+97+116=312,hash值:312%11=4
代码如下:
def hash(astring, tablesize):
sum = 0
for pos in range(len(astring)):
sum = sum + ord(astring[pos])
return sum%tablesize
3.2 Collision Resolution
为了解决不同元素在hash function下对应了相同的hash值。一般的方法有:linear probing 、quadratic probing 和chaining
linear probing:核心思想是在相同的hash值下从头寻找hash table中的空闲值,并存储该元素。
如要对列表[54,26,9,17,77,31,44,55,20]用hash table进行存储,得到的hash table如下:
为了避免有冲突的值都大面积的聚集在一块,可以使用添加一个步长的方法来计算hash值:
如采用上述的rehash后得到的hash table:
quadratic probing:是linear probing的一个变体,即用变化的步长来代替固定的步长
chaining:在冲突的hash值下,进行链式的存储,从而来解决冲突。
3.3 实现map的抽象数据结构
主要实现的功能是能够在计算代价为O(1)的情况下进行数据的搜索。该数据结构含有的基本功能包括:创建Map(),存储元素put(key,value),获取元素get(key),删除元素del map[key],获取元素的长度len(),判断元素是否在map中key in map。
在python中使用两个列表来存储key和value,即初始化函数可以如下:
class HashTable:
def __init__(self):
self.size = 11
self.slots = [None] * self.size
self.data = [None] * self.size
根据之前的hash function和rehash的方法来进行hash值的求解,进而存储数据,进行了冲突的解决:
def put(self,key,data):
hashvalue = self.hashfunction(key,len(self.slots))
if self.slots[hashvalue] == None:
self.slots[hashvalue] = key
self.data[hashvalue] = data
else:
if self.slots[hashvalue] == key:
self.data[hashvalue] = data #replace
else:
nextslot = self.rehash(hashvalue,len(self.slots))
while self.slots[nextslot] != None and \
self.slots[nextslot] != key:
nextslot = self.rehash(nextslot,len(self.slots))
if self.slots[nextslot] == None:
self.slots[nextslot]=key
self.data[nextslot]=data
else:
self.data[nextslot] = data #replace
def hashfunction(self,key,size):
return key%size
def rehash(self,oldhash,size):
return (oldhash+1)%size
在获取数据的函数中,也需要考虑到hash resolution的作用,通过rehash函数来重新计算hash值,直到找到对应的key,或者遍历了所有可能的hash值而没有找到最终结果:
def get(self,key):
startslot = self.hashfunction(key,len(self.slots))
data = None
stop = False
found = False
position = startslot
while self.slots[position] != None and \
not found and not stop:
if self.slots[position] == key:
found = True
data = self.data[position]
else:
position=self.rehash(position,len(self.slots))
if position == startslot:
stop = True
return data
def __getitem__(self,key):
return self.get(key)
def __setitem__(self,key,data):
self.put(key,data)
一个演示结果如下:
>>> H=HashTable()
>>> H[54]="cat"
>>> H[26]="dog"
>>> H[93]="lion"
>>> H[17]="tiger"
>>> H[77]="bird"
>>> H[31]="cow"
>>> H[44]="goat"
>>> H[55]="pig"
>>> H[20]="chicken"
>>> H.slots
[77, 44, 55, 20, 26, 93, 17, None, None, 31, 54]
>>> H.data
['bird', 'goat', 'pig', 'chicken', 'dog', 'lion',
'tiger', None, None, 'cow', 'cat']
-
Sorting
根据某种规则对列表进行排序,如常见的降序和升序等。
1. 冒泡算法(Bubble Sort)
冒泡算法时排序中一个很经典的算法,通过列表中的数据进行两两组合,再进行比较,把较大的一个数向后交换,经过n-1次相同的操作之后,将会得到一个升序排列的序列,例如第一次的操作如下:
接下对前n-1个数进行相同的操作,得到第二次的操作,即排序好了第二个数。代码如下:
def bubbleSort(alist):
for passnum in range(len(alist)-1,0,-1):
for i in range(passnum):
if alist[i]>alist[i+1]:
alist[i],alist[i+1] = alist[i+1], alist[i]
但是这个冒泡排序会有一些多余的操作,如在前面几个操作就完成了排序之后,算法还是会继续进行直到进行了全部的n-1次。
一个改进的方式是当排序完成之后就不需要进行后面的操作了,来提前结束算法,即早停。
def shortBubbleSort(alist):
exchanges = True
passnum = len(alist)-1
while passnum > 0 and exchanges:
exchanges = False
for i in range(passnum):
if alist[i]>alist[i+1]:
exchanges = True
temp = alist[i]
alist[i] = alist[i+1]
alist[i+1] = temp
passnum = passnum-1
时间代价为O(n*n)
2.选择排序(selection sort)
因为在冒泡算法中有大量的交换操作,而这些操作其实可以没有必要的。只要找到当前操作下的最大值进行在特定位置上交换即可。
算法如下:
def selectionSort(alist):
for fillslot in range(len(alist)-1,0,-1):
positionOfMax=0
for location in range(1,fillslot+1):
if alist[location]>alist[positionOfMax]:
positionOfMax = location
temp = alist[fillslot]
alist[fillslot] = alist[positionOfMax]
alist[positionOfMax] = temp
alist = [54,26,93,17,77,31,44,55,20]
因为只是减少了交换的次数,所以时间代价还是O(n*n)
3.插入排序(Insertion Sort)
插入排序的时间复杂度还是O(n*n),只是相比于其他算法只是工作原理不太一样,工作流程如下:
每次对下一个数据进行插入,插入到适当的位置处,在插入时实现的算法如下:
对已经排序好的子序列中的数值进行移位,在判断是正确位置时,插入即可,插入步骤结束。
def insertionSort(alist):
for index in range(1,len(alist)):
currentvalue = alist[index]
position = index
while position>0 and alist[position-1]>currentvalue:
alist[position]=alist[position-1]
position = position-1
alist[position]=currentvalue
4.希尔排序(Shell Sort)
希尔排序也成为减小增量排序,让原序列变成多个子序列,然后对子序列进行插入排序,再融合成大的序列:
接着进行下一步的插入排序,可以使用1增量的排序,或者其他增量的排序,最终排序成功即可,极大的减小了交换的次数:
def shellSort(alist):
sublistcount = len(alist)//2
while sublistcount > 0:
for startposition in range(sublistcount):
gapInsertionSort(alist,startposition,sublistcount)
print("After increments of size",sublistcount,
"The list is",alist)
sublistcount = sublistcount // 2
def gapInsertionSort(alist,start,gap):
for i in range(start+gap,len(alist),gap):
currentvalue = alist[i]
position = i
while position>=gap and alist[position-gap]>currentvalue:
alist[position]=alist[position-gap]
position = position-gap
alist[position]=currentvalue
alist = [54,26,93,17,77,31,44,55,20]
shellSort(alist)
print(alist)
shell sort 的时间代价为:
5.融合排序(merge sort)
主要包括了两个部分:分割(split)和融合(merge)过程。
分割是让序列对半分,分到只有一个元素即可。融合是对分割后的子序列进行排序融合,得到一个更大的子序列。该算法是使用递归实现的,显然其中的base case 就是子序列中不含有元素或者只含有一个元素。分割和融合的过程如下:
算法如下:
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)
alist = [54,26,93,17,77,31,44,55,20]
mergeSort(alist)
print(alist)
算法的输出如下:
从输出和流程图都能够看出,算法是单边进行分割和单边进行融合,在实现最终两侧的融合。分割过程中也存在不均匀分割。其中的算法时间复杂度为O(nlogn)。
6.快排(quick sort)
快排的核心思想是根据一个轴值(pivot value)来进行寻找序列中的该轴值所处的位置,并把该位置当做一个分隔点,进而实现把序列进行切割,在切割完的序列中再进行相似的操作,直到排完序。
例如在序列中的第一个位置当做轴值来寻找分割点,其流程如下:
通过leftmark来寻找大于轴值的值, 通过rightmark来寻找小于轴值的值,从而进行交换,再重复这个过程,结束的标准是rightmark小于了leftmark,即实现了全序列的搜索了,则该轴值应该替代rightmark所在位置的值。于是进行分割,得到两个序列
接着对这两个序列进行递归的调用上述的寻找分割点的过程。显然这是一个递归的过程,算法如下:
def quickSort(alist):
quickSortHelper(alist,0,len(alist)-1)
def quickSortHelper(alist,first,last):
if first<last:
splitpoint = partition(alist,first,last)
quickSortHelper(alist,first,splitpoint-1)
quickSortHelper(alist,splitpoint+1,last)
def partition(alist,first,last):
pivotvalue = alist[first]
leftmark = first+1
rightmark = last
done = False
while not done:
while leftmark <= rightmark and alist[leftmark] <= pivotvalue:
leftmark = leftmark + 1
while alist[rightmark] >= pivotvalue and rightmark >= leftmark:
rightmark = rightmark -1
if rightmark < leftmark:
done = True
else:
temp = alist[leftmark]
alist[leftmark] = alist[rightmark]
alist[rightmark] = temp
temp = alist[first]
alist[first] = alist[rightmark]
alist[rightmark] = temp
return rightmark
alist = [54,26,93,17,77,31,44,55,20]
quickSort(alist)
print(alist)
其中的base case就是进行寻找分割点,在leftmark小于rightmark的时候。
算法的时间代价,因为算法的性能与轴值的选择有关,当轴值能够很好的对序列进行平均分配时,时间复杂度为O(nlogn)。
但不能够对其完美的分割时,假设每次分割的都是一个元素和剩余的元素,则时间复杂度为O(n*n)。
这些就是本次学习中学习到的所有搜索算法和排序算法了,应该算是很经典的算法了、
对其时间复杂度的总结如下: