数据结构之排序算法
一.冒泡排序
对于一个数组,从前往后依次比较相邻的两个元素,将较大的元素放在较小的元素的后面。
假设数组的长度为n,第一次比较需要比较n-1次,第二次在剩下的n-1个元素里面找最大值,需要比较n-2次......直到比较到还剩下一个元素,这时剩下的这个元素就一定是最小的,从小到大的排序任务就完成了。这个过程中一共要遍历数组n-1次(只剩下一个元素时这种情况不用比较,也就不用遍历,所以遍历数组的次数是n-1),而且每次遍历数组时要遍历的长度都在变小,第一次需要全部遍历,第一次遍历完后已经找到了n个元素中的最大值并把它放到了数组的末尾,第二次就不需要比较这个最大的元素了,只需要在剩下的n-1个元素里找次大的,所以第二次遍历数组时不用全部都遍历,只需要遍历n-1个长度,即从第1个元素遍历到倒数第二个元素(第n-1个);然后依次类推
def bubble_sort(arr):n = len(arr)for i in range(n-1):for j in range(n-i-1):if arr[j] > arr[j+1]:arr[j], arr[j+1] = arr[j+1], arr[j]return arrdef bubble_sort_short(arr):"""短冒泡排序"""n = len(arr)for i in range(n-1):swapped = False # 优化:提前终止标志 值为False说明该轮比较中没有元素交换for j in range(1, n-i):if arr[j] < arr[j-1]:arr[j], arr[j-1] = arr[j-1], arr[j] swapped = Trueif not swapped: # 如果本轮比较没有交换,则说明数组已经有序,直接退出循环breakif __name__ == "__main__":result = bubble_sort([2,3,1,0,6,-4,9,-6])print(result)a_list = [4,2,-1,9,45,76,-99,3]bubble_sort_short(a_list) # 进行原地排序print(a_list)
输出:
[-6, -4, 0, 1, 2, 3, 6, 9]
[-99, -1, 2, 3, 4, 9, 45, 76]
时间复杂度分析:
最好情况下只需比较1次,时间复杂度为o(1)
最坏情况下要遍历n-1次,比较次数依次为:n-1, n-2, n-3, ... , 3, 2, 1 。共要比较次,所以时间复杂度为o(
)
平均情况下要比较 除以 (n-1)也就是
次,所以时间复杂度为o(n)
二.选择排序
选择排序是在冒泡排序的基础上进行了一些改进。改进的地方在于每次遍历数组时只交换一次
要实现这一点,就要在每次遍历数组时找出数组的最大值,然后在该次遍历完之后把这个最大值放在合适的位置上
假设数组元素个数为n,只需遍历n-1次,找出n-1个较大值,剩下的那个元素自然就有序了
def selected_sort_min(arr):"""找最小值,把最小值放到数组的第一个位置"""n = len(arr)for i in range(n-1):min_idx = ifor j in range(i+1,n):if arr[j] < arr[min_idx]:min_idx = jarr[i], arr[min_idx] = arr[min_idx], arr[i]return arrdef selected_sort_max(arr):"""找最大值,把最大值放到数组的末尾"""n = len(arr)for i in range(n-1,0,-1): # 序列为(n-1, n-2, n-3, ..., 2, 1)max_idx = ifor j in range(i):if arr[j] > arr[max_idx]:max_idx = jif max_idx != i: # 只有位置不同时才交换,位置相同说明该次遍历中没有元素比该元素大arr[i], arr[max_idx] = arr[max_idx], arr[i]return arrif __name__ == "__main__":result = selected_sort_min([1,5,6,2,-6,7,-9,4])print(result)result = selected_sort_max([3,1,9,0,4,-5,5])print(result)
输出:
[-9, -6, 1, 2, 4, 5, 6, 7]
[-5, 0, 1, 3, 4, 5, 9]
时间复杂度分析:
最坏情况下:同冒泡排序一样,为o()
三.插入排序
基本思想:
- 将数组分为“已排序”和“未排序”两部分
- 每次从数组中的“未排序”部分取出一个元素
- 将取出的这个元素插入到“已排序部分”的正确位置
- 重复这个过程直到“未排序”部分的所有元素都被插入到“已排序”部分中
def insertion_sort(arr):n = len(arr)for i in range(1,n):key = arr[i]j = i - 1while j >= 0 and arr[j] > key:arr[j+1] = arr[j] j -= 1arr[j+1] = keyreturn arrdef insertion_sort_swap(arr):n = len(arr)for i in range(1,n):j = i while j > 0 and arr[j] < arr[j-1]:arr[j], arr[j-1] = arr[j-1], arr[j]j -= 1return arrdef binary_insertion_sort(arr):"""二分插入排序 - 减少比较次数"""n = len(arr)for i in range(1, n):key = arr[i]# 使用二分查找找到插入位置left, right = 0, i - 1while left <= right:mid =(left + right) // 2if arr[mid] > key:right = mid - 1else:left = mid + 1# 将元素向右移动,为插入腾出空间for j in range(i - 1, left - 1, -1):arr[j + 1] = arr[j]# 插入元素arr[left] = keyreturn arrif __name__ == "__main__":result = insertion_sort([2,4,1,5,-9,8,3])result_1 = insertion_sort_swap([1,4,98,-6,0,3,-9])result_2 = binary_insertion_sort([3,5,9,0,7,-5,2,6])print(result)print(result_1)print(result_2)
四.希尔排序
插入排序的改进版本,也称为“缩小增量排序”
核心思想:通过将原始列表分割为多个子序列,分别对这些子序列进行插入排序,从而使得整个列表逐渐趋于“基本有序”,最后再对整个列表进行一次插入排序。
这样做的原理在于插入排序在对“基本有序”的列表进行排序时,效率非常高,接近o(n)
工作原理:
- 选择增量序列:首先确定一个增量序列。增量是一个间隔,用于将列表划分为子序列。最常用且最简单的增量是初始增量=列表长度/2,然后每次再减半,直到增量为1
- 按增量分组并排序:根据当前增量,将列表分割成多个子序列。每个子序列由相隔“增量”位置的元素组成,然后对每个子序列分别进行插入排序
- 缩小增量:减小增量的值(如减半),重复步骤二
- 最终排序:当增量减小到1时,整个列表被视为一个子序列,进行最后一次插入排序。此时,由于列表已经基本有序,这次插入排序会非常快
举例说明:
对数组[8,3,5,1,4,7,2,6]进行希尔排序
初始数组为[8,3,5,1,4,7,2,6]
长度n=8,选择增量序列gap=n/2, n/4, n/8,.... 即4,2,1
第一轮循环:
增量为4
子序列1:[8,4] ---> [4,8]
子序列2:[3,7] ---> [3,7]
子序列3:[5,2] ---> [2,5]
子序列4:[1,6] --- > [1,6]
分别对这四个子序列进行插入排序,得到右边的排好序的子序列
在将这些排好序的子序列按照原来的位置放回原始数组
得到:
[4,3,2,1,8,7,5,6]
第二轮循环:
增量为2
子序列分别为[4,2,8,5] [3,1,7,6]
分别对这两个子序列进行插入排序得到排好序的子序列:[2,4,5,8] [1,3,6,7]
第二轮循环得到的数组:[2,1,4,3,5,6,8,7]
第三轮循环:
增量为1,此时整个数组就是一个子序列
再对其进行插入排序,得到最终的排好序的数组:[1,2,3,4,5,6,7,8]
def shell_sort(arr):n = len(arr)# 初始增量gap = n // 2# 循环直到gap缩小到0while gap > 0:# 从gap开始,对每个子序列进行插入排序for i in range(gap,n):temp = arr[i] # 当前要插入的元素j = i# 对子序列进行插入排序# j >= gap 确保不越界# arr[j - gap] > temp 是插入排序的比较条件while j >= gap and arr[j - gap] > temp:arr[j] = arr[j - gap] # 向后移动元素j -= gaparr[j] = temp # 插入到正确位置# 缩小增量gap //= 2return arrif __name__ == "__main__":print(shell_sort([2,7,4,-7,9,6,-9,5]))
稳定性:希尔排序不稳定,在排序过程中,相等的原始可能会因为分属不同的子序列而改变相对次序
最坏情况下的时间复杂度:o(n^2)
最好情况下的时间复杂度: o(nlogn)