本文涉及到的代码均已放置在我的github中 -->链接
python实现基础的数据结构(一)
python实现基础的数据结构(二)
python实现基础的数据结构(三)
python语法较为简洁,正好最近在复习数据结构,索性就用python去实现吧?
本文实现的有线性表、栈、队列、串、二叉树、图、排序算法。参照教材为数据结构(C语言版)清华大学出版社
,还有网上的一些大神的见解。由于包含代码在内,所以内容很多,分了几篇,话不多说,我们直接步入正题?
注:本文实验环境:Anaconda 集成 python 3.6.5 ( 故代码为 python3 )
由于本文代码过多,故每一步不做太详细的介绍,我尽量在代码中解释细节,望体谅
排序算法
分为外部排序和内部排序,我们这里只讲内部排序,指的是将待排记录存放在计算机随机存储器中进行的排序过程,常用的排序算法一共有十种,前六种是常用到的,后四种仅供了解便可,稳定不稳定是看代码执行过程中会不会把原来已有部分符合要求的顺序破坏掉,例如:5,1,2…在执行过程中若有1和2交换就叫做不稳定。先说结论吧?
(下面的动图摘自https://blog.csdn.net/zhangshk_/article/details/82911093,先感谢这位大佬,直观清晰弄懂排序)
排序方法 平均时间 最好时间 最坏时间
冒泡排序(稳定) O(n^2) O(n) O(n^2)
选择排序(不稳定)O(n^2) O(n^2) O(n^2)
插入排序(稳定) O(n^2) O(n) O(n^2)
希尔排序(不稳定)O(n^1.25)
归并排序(稳定) O(nlogn) O(nlogn) O(nlogn)
快速排序(不稳定)O(nlogn) O(nlogn) O(n^2)
堆排序(不稳定) O(nlogn) O(nlogn) O(nlogn)
计数排序(稳定) O(n)
桶排序(不稳定) O(n) O(n) O(n)
基数排序(稳定) O(n) O(n) O(n)
附:O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n)
最快的排序算法是桶排序,但缺陷很大,所以快速排序用的较多
冒泡排序
它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果他们的顺序(如从大到小、首字母从A到Z)错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素列已经排序完成。
这个算法的名字由来是因为越大的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名“冒泡排序”。
冒泡排序较为简单,代码:
def bubbleSort(nums):
'''冒泡排序 ''
for i in range(len(nums) - 1):
for j in range(len(nums) - i - 1): # 已排好序的部分不用再次遍历
if nums[j] > nums[j+1]:
nums[j], nums[j+1] = nums[j+1], nums[j]
return nums
选择排序
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。选择排序是不稳定的排序方法。
代码?
def selectionSort(nums):
'''选择排序'''
for i in range(len(nums) - 1): # 遍历 len(nums)-1 次
minIndex = i
for j in range(i + 1, len(nums)):
if nums[j] < nums[minIndex]: # 更新最小值索引
minIndex = j
nums[i], nums[minIndex] = nums[minIndex], nums[i] # 把最小数交换到前面
return nums
插入排序
插入排序(Insertion sort)是一种简单直观且稳定的排序算法。基本操作是讲一个记录插入到已经排好序的有序表中,从而得到一个新的有序表。插入排序的基本思想是:每步将一个待排序的记录,按其关键码值的大小插入前面已经排序的文件中适当位置上,直到全部插入完为止。是一种稳定的排序算法。?
代码如下:
def insertionSort(nums):
'''插入排序'''
for i in range(len(nums) - 1): # 遍历 len(nums)-1 次
curNum, preIndex = nums[i+1], i # curNum 保存当前待插入的数
while preIndex >= 0 and curNum < nums[preIndex]: # 将比 curNum 大的元素向后移动
nums[preIndex + 1] = nums[preIndex]
preIndex -= 1
nums[preIndex + 1] = curNum # 待插入的数的正确位置
return nums
希尔排序
顾名思义,这是希尔提出来的算法,它是插入排序的改进版本,也称递减增量排序算法。基本思想是:先将整个待排记录序列分隔成为若干子序列分别进行直接插入排序,待整个序列中的记录基本有序时,再对全体记录进行一次直接插入排序。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位
附上代码?:
def shellSort(nums):
'''希尔排序'''
lens = len(nums)
gap = 1
while gap < lens // 3:
gap = gap * 3 + 1 # 动态定义间隔序列
while gap > 0:
for i in range(gap, lens):
curNum, preIndex = nums[i], i - gap # curNum 保存当前待插入的数
while preIndex >= 0 and curNum < nums[preIndex]:
nums[preIndex + gap] = nums[preIndex] # 将比 curNum 大的元素向后移动
preIndex -= gap
nums[preIndex + gap] = curNum # 待插入的数的正确位置
gap //= 3 # 下一个动态间隔
return nums
归并排序
“归并”的意思是将两个或者两个以上的有序表合并成为一个新的有序表,这是采用分治法最典型的应用之一,在以后的文章中也可能会出现关于分治法的算法解释,值得一提的是,该算法是冯诺依曼提出的。基本思想与分治法差不多:
- 分割:递归地把当前序列平均分割成两半。
- 集成:在保持元素顺序的同时将上一步得到的子序列集成到一起(归并)。
代码如下:
def mergeSort(nums):
'''归并排序'''
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 = result + left[i:] + right[j:] # 剩余的元素直接添加到末尾
return result
if len(nums) <= 1:
return nums
mid = len(nums) // 2
left = mergeSort(nums[:mid])
right = mergeSort(nums[mid:])
return merge(left, right)
快速排序
这是当今使用最多的排序算法了,基本上在大数据里面都是使用的这种排序方法。它是对冒泡排序的一种改进算法,基本思想是:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分得记录继续排序,以达到整个序列有序。它也是分治法的一个应用,是目前使用最广泛的排序算法,但注意这是一个不稳定排序
代码如下:
def quickSort(nums): # 这种写法的平均空间复杂度为 O(nlogn)
'''快速排序'''
if len(nums) <= 1:
return nums
pivot = nums[0] # 基准值
left = [nums[i] for i in range(1, len(nums)) if nums[i] < pivot]
right = [nums[i] for i in range(1, len(nums)) if nums[i] >= pivot]
return quickSort(left) + [pivot] + quickSort(right)
堆排序
堆排序指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构。堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。
这个大家直接看图吧,可能不好理解,图个乐就行?
代码如下:
def heapSort(nums):
'''堆排序'''
# 调整堆
def adjustHeap(nums, i, size):
# 非叶子结点的左右两个孩子
lchild = 2 * i + 1
rchild = 2 * i + 2
# 在当前结点、左孩子、右孩子中找到最大元素的索引
largest = i
if lchild < size and nums[lchild] > nums[largest]:
largest = lchild
if rchild < size and nums[rchild] > nums[largest]:
largest = rchild
# 如果最大元素的索引不是当前结点,把大的结点交换到上面,继续调整堆
if largest != i:
nums[largest], nums[i] = nums[i], nums[largest]
# 第 2 个参数传入 largest 的索引是交换前大数字对应的索引
# 交换后该索引对应的是小数字,应该把该小数字向下调整
adjustHeap(nums, largest, size)
# 建立堆
def builtHeap(nums, size):
for i in range(len(nums)//2)[::-1]: # 从倒数第一个非叶子结点开始建立大根堆
adjustHeap(nums, i, size) # 对所有非叶子结点进行堆的调整
# print(nums) # 第一次建立好的大根堆
# 堆排序
size = len(nums)
builtHeap(nums, size)
for i in range(len(nums))[::-1]:
# 每次根结点都是最大的数,最大数放到后面
nums[0], nums[i] = nums[i], nums[0]
# 交换完后还需要继续调整堆,只需调整根节点,此时数组的 size 不包括已经排序好的数
adjustHeap(nums, 0, i)
return nums # 由于每次大的都会放到后面,因此最后的 nums 是从小到大排列
计数排序
计数排序(Counting sort)是一种稳定的线性时间排序算法。计数排序使用一个额外的数组 C ,其中第i个元素是待排序数组A中值等于i的元素的个数。然后根据数组 C 来将A中的元素排到正确的位置。计数排序不是比较排序,排序的速度快于任何比较排序算法。这种算法虽然比较快,但是限制条件很多,所以使用比较少,了解便可
通俗地理解,例如有10个年龄不同的人,统计出有8个人的年龄比A小,那A的年龄就排在第9位,用这个方法可以得到其他每个人的位置,也就排好了序。当然,年龄有重复时需要特殊处理(保证稳定性),这就是为什么最后要反向填充目标数组,以及将每个数字的统计减去1。算法的步骤如下:
- 找出待排序的数组中最大和最小的元素
- 统计数组中每个值为i的元素出现的次数,存入数组 C的第i项
- 对所有的计数累加(从C 中的第一个元素开始,每一项和前一项相加)
- 反向填充目标数组:将每个元素i放在新数组的第C[i]项,每放一个元素就将 C[i]减去1.
代码如下:
def countingSort(nums):
'''计数排序'''
bucket = [0] * (max(nums) + 1) # 桶的个数
for num in nums: # 将元素值作为键值存储在桶中,记录其出现的次数
bucket[num] += 1
i = 0 # nums 的索引
for j in range(len(bucket)):
while bucket[j] > 0:
nums[i] = j
bucket[j] -= 1
i += 1
return nums
桶排序
桶排序 (Bucket sort)或所谓的箱排序,是一个排序算法,工作的原理是将数组分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。桶排序并不是 比较排序,桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。
桶排序应该是最快的排序方法了,但因其缺陷很大,常用的还是快速排序?
代码如下:
def bucketSort(nums, defaultBucketSize = 5):
'''桶排序'''
maxVal, minVal = max(nums), min(nums)
bucketSize = defaultBucketSize # 如果没有指定桶的大小,则默认为5
bucketCount = (maxVal - minVal) // bucketSize + 1 # 数据分为 bucketCount 组
buckets = [] # 二维桶
for i in range(bucketCount):
buckets.append([])
# 利用函数映射将各个数据放入对应的桶中
for num in nums:
buckets[(num - minVal) // bucketSize].append(num)
nums.clear() # 清空 nums
# 对每一个二维桶中的元素进行排序
for bucket in buckets:
insertionSort(bucket) # 假设使用插入排序
nums.extend(bucket) # 将排序好的桶依次放入到 nums 中
return nums
基数排序
终于到最后一个了,累死我了?。基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较,它是这样实现的:将所有待比较数值(正整数)统一为同样的数字长度,数字较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。与桶排序有类似之处,看图
代码如下:
def radixSort(nums):
'''基数排序'''
mod = 10
div = 1
mostBit = len(str(max(nums))) # 最大数的位数决定了外循环多少次
buckets = [[] for row in range(mod)] # 构造 mod 个空桶
while mostBit:
for num in nums: # 将数据放入对应的桶中
buckets[num // div % mod].append(num)
i = 0 # nums 的索引
for bucket in buckets: # 将数据收集起来
while bucket:
nums[i] = bucket.pop(0) # 依次取出
i += 1
div *= 10
mostBit -= 1
return nums
关于测试
我添加了时间模块去大致比较了一下耗时情况,先上测试代码(时间库是 time)
data_test = [23,1,53,654,54,16,65,3,155,506,10, 164, 234, 31, 3, 54,46,654,315]
print('跟在结果后面的是运行时长(s)')
print('冒泡排序:')
start = time.time()
print(bubbleSort(data_test))
end = time.time()
print(end-start)
print()
print('选择排序:')
start = time.time()
print(selectionSort(data_test))
end = time.time()
print(end-start)
print()
print('插入排序:')
start = time.time()
print(insertionSort(data_test))
end = time.time()
print(end-start)
print()
print('希尔排序:')
start = time.time()
print(shellSort(data_test))
end = time.time()
print(end-start)
print()
print('归并排序:')
start = time.time()
print(mergeSort(data_test))
end = time.time()
print(end-start)
print()
print('快速排序:')
start = time.time()
print(quickSort(data_test))
end = time.time()
print(end-start)
print()
print('堆排序:')
start = time.time()
print(heapSort(data_test))
end = time.time()
print(end-start)
print()
print('计数排序:')
start = time.time()
print(countingSort(data_test))
end = time.time()
print(end-start)
print()
print('桶排序:')
start = time.time()
print(bucketSort(data_test))
end = time.time()
print(end-start)
print()
print('基数排序:')
start = time.time()
print(radixSort(data_test))
end = time.time()
print(end-start)
运行结果:
由于我这里测试的数据量太小,而且可能因为电脑的原因,所以效果不会太明显,但多次运行后,还是发现快速排序是最稳定的
完。。。休息?️