搜索和排序算法学习汇总:search & sort algorithm

  • 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)。

 

这些就是本次学习中学习到的所有搜索算法和排序算法了,应该算是很经典的算法了、

对其时间复杂度的总结如下:

                 

 

 

 

 

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值