常见排序算法的时间和空间复杂度
下面是常见排序算法的时间复杂度和空间复杂度,包括辅助存储的情况:
排序算法 | 平均时间复杂度 | 最坏时间复杂度 | 最好时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(n^2) | O(n^2) | O(n) | O(1) | 稳定 |
选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
插入排序 | O(n^2) | O(n^2) | O(n) | O(1) | 稳定 |
希尔排序 | O(n log^2 n) | O(n log^2 n) | O(n log n) | O(1) | 不稳定 |
归并排序 | O(n log n) | O(n log n) | O(n log n) | O(n) | 稳定 |
快速排序 | O(n log n) | O(n^2) | O(n log n) | O(log n) | 不稳定 |
堆排序 | O(n log n) | O(n log n) | O(n log n) | O(1) | 不稳定 |
计数排序 | O(n + k) | O(n + k) | O(n + k) | O(n + k) | 稳定 |
桶排序 | O(n + k) | O(n^2) | O(n) | O(n + k) | 稳定 |
基数排序 | O(n * k) | O(n * k) | O(n * k) | O(n + k) | 稳定 |
注意,辅助存储指的是除了原始输入数据外,算法额外使用的存储空间。对于归并排序、计数排序、桶排序和基数排序,它们都需要额外的存储空间来存储中间结果,因此它们的空间复杂度会比较高。而对于其他排序算法,它们通常是在原地进行排序,即不需要额外的存储空间,因此它们的空间复杂度为 O(1)。
希尔排序的时间复杂度通常表示为 O(n^1.3) 或 O(n^1.5)。这是因为实际使用中采用的增量序列通常是经过优化得出的,比如希尔增量序列或者 Sedgewick 增量序列,它们的复杂度接近于 O(n^1.3) 或 O(n^1.5)。但由于增量序列的选择不唯一,也可以得到其他更接近的时间复杂度。所以希尔排序的时间复杂度可以近似地表示为 O(n^1.3) 或 O(n^1.5)。空间复杂度仍然为 O(1)。
边界
当谈到算法的边界时,通常是指算法的边界条件或边界情况。边界条件是指问题中可能出现的特殊情况,通常在输入数据的边界或特殊情况下进行考虑和处理。对于边界情况的正确处理对算法的正确性和鲁棒性非常重要。
以下是一些常见的边界情况的例子:
-
空输入:当输入为空集合、空字符串或空数组时,算法需要能够正确处理这种情况,并给出合适的输出或返回结果。
-
边界值:当输入数据的值处于边界情况时,算法需要能够正确处理。例如,对于排序算法,如果输入的数组长度为0或1,或者数组中的所有元素已经有序,则需要特殊处理。
-
最大/最小输入:对于一些问题,当输入数据的大小达到最大限制时,算法的性能可能会受到影响。在这种情况下,算法需要能够高效地处理最大规模的输入。
-
特殊字符和边界符号:在处理字符串或文本时,可能会遇到特殊字符、空格、标点符号等。算法需要能够正确处理这些字符,并避免产生错误或异常结果。
边界情况的考虑和处理通常需要在算法设计和实现的过程中进行测试和验证。通过边界情况的测试,可以确保算法在各种输入情况下都能正确运行,并具有良好的鲁棒性。在实现算法时,应特别关注边界情况,并根据问题的特性进行相应的处理。
排序
不同排序算法的边界条件主要涉及输入数据的规模、数据类型和特殊情况。以下是一些常见排序算法的边界情况:
-
冒泡排序(Bubble Sort):
- 边界情况:当输入数组为空或只包含一个元素时,无需进行排序,直接返回输入数组。
- 边界值:当数组已经有序时,冒泡排序在最好情况下时间复杂度为O(n)。
-
选择排序(Selection Sort):
- 边界情况:当输入数组为空或只包含一个元素时,无需进行排序,直接返回输入数组。
- 边界值:无特殊边界值情况,时间复杂度始终为O(n^2)。
-
插入排序(Insertion Sort):
- 边界情况:当输入数组为空或只包含一个元素时,无需进行排序,直接返回输入数组。
- 边界值:当数组已经有序时,插入排序在最好情况下时间复杂度为O(n)。
-
归并排序(Merge Sort):
- 边界情况:当输入数组为空或只包含一个元素时,无需进行排序,直接返回输入数组。
- 边界值:在合并阶段,如果左侧或右侧的数组为空,则直接将另一侧数组合并到结果中。
-
快速排序(Quick Sort):
- 边界情况:当输入数组为空或只包含一个元素时,无需进行排序,直接返回输入数组。
- 边界值:在划分过程中,如果待排序的子数组只包含一个元素或为空,无需进一步划分。
-
堆排序(Heap Sort):
- 边界情况:当输入数组为空或只包含一个元素时,无需进行排序,直接返回输入数组。
- 边界值:在构建堆的过程中,如果堆的大小为0或1,无需进行调整。
-
计数排序(Counting Sort):
- 边界情况:计数排序通常用于非负整数的排序,当输入数组为空或只包含一个元素时,无需进行排序,直接返回输入数组。
- 边界值:对于计数排序,要求输入的最大值不过大,避免耗费过多内存。
-
基数排序(Radix Sort):
- 边界情况:基数排序通常用于非负整数的排序,当输入数组为空或只包含一个元素时,无需进行排序,直接返回输入数组。
- 边界值:对于基数排序,要求输入的最大值不过大,避免耗费过多内存。
-
希尔排序(Shell Sort)是一种基于插入排序的排序算法,也称为缩小增量排序。它通过将原始数组分割成多个较小的子数组,并对每个子数组进行插入排序。然后逐步缩小子数组的规模,直到最后整个数组都被排序。希尔排序的主要思想是通过设置一个增量(间隔)序列来对数组进行分组和排序。通常初始增量的选择是数组长度的一半,并逐步缩小增量,直到增量为1。这样可以使得数组在较早的阶段基本有序,从而减少插入排序的工作量,提高排序效率。
需要注意的是,边界情况并不仅限于上述提到的情况,具体的边界条件可能因排序算法的实现
方式和问题的特定要求而有所不同。在实现排序算法时,应该充分考虑边界情况,并进行相应的处理,以确保算法的正确性和鲁棒性。
冒泡排序
冒泡排序算法的核心思想是通过相邻元素的比较和交换,将较大的元素逐步“冒泡”到数组的末尾。在每一轮循环中,通过比较相邻的两个元素,将较大的元素向右交换,使得较大的元素逐渐移动到正确的位置。
冒泡排序的核心包括以下几个要点:
-
比较相邻元素:在每一轮循环中,比较相邻的两个元素的大小关系,通过判断它们的顺序来决定是否需要进行交换。
-
交换元素位置:如果两个相邻元素的顺序不正确(比较顺序为升序时,前面的元素大于后面的元素),则进行交换,将较大的元素向右移动。
-
多轮循环:冒泡排序需要进行多轮循环,每一轮循环将当前未排序部分中最大的元素冒泡到最右边。每经过一轮循环,待排序部分的长度减少1。
-
优化标志:为了提高冒泡排序的效率,在每一轮循环中,可以设置一个标志变量,如果某一轮循环中没有进行任何交换,即未发生元素的交换,说明数组已经有序,可以提前结束排序。
冒泡排序的核心思想可以总结为:通过相邻元素的比较和交换,逐步将较大的元素“冒泡”到正确的位置,重复这个过程,直到整个数组有序。
冒泡排序是一种简单的排序算法,它通过多次交换相邻元素的位置来将最大(或最小)的元素逐渐“冒泡”到正确的位置。下面介绍几种冒泡排序的写法:
- 基本冒泡排序:
def bubble_sort(arr):
n = len(arr)
for i in range(n - 1):
for j in range(n - 1 - i):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
return arr
- 优化版冒泡排序,加入标志位:
def bubble_sort(arr):
n = len(arr)
for i in range(n - 1):
flag = False
for j in range(n - 1 - i):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
flag = True
if not flag:
break
return arr
- 另一种优化版冒泡排序,记录上一次交换的位置:
def bubble_sort(arr):
n = len(arr)
last_swap = n - 1
while last_swap > 0:
k = last_swap
last_swap = 0
for j in range(k):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
last_swap = j
return arr
这些都是冒泡排序的不同写法,它们的核心思想都是通过相邻元素的比较和交换来完成排序。其中,优化版的冒泡排序可以提前结束排序过程,如果某次遍历没有进行任何交换,则说明数组已经有序,无需继续循环。这样可以减少不必要的比较和交换操作,提高冒泡排序的效率。
插入排序
插入排序(Insertion Sort)是一种简单的排序算法,其核心思想是将未排序的元素逐个插入到已排序的部分中,使得已排序部分始终保持有序。插入排序在实现上较为简单,适用于小规模的数据排序。
算法步骤:
- 从第二个元素开始,依次将其插入已排序部分。
- 对于当前元素,将其与已排序部分从右向左逐个比较。
- 如果当前元素比已排序部分中的元素小,则将已排序部分中的元素右移一位,空出位置。
- 继续向左比较,直到找到合适的位置将当前元素插入。
- 将当前元素插入到找到的位置。
以下是插入排序的Python实现代码:
def insertion_sort(arr):
n = len(arr)
for i in range(1, n):
key = arr[i]
j = i - 1
while j >= 0 and arr[j] > key:
arr[j + 1] = arr[j]
j = j - 1
arr[j + 1] = key
# 示例
arr = [5, 2, 8, 3, 1]
insertion_sort(arr)
print(arr) # 输出:[1, 2, 3, 5, 8]
选择排序
选择排序是一种简单直观的排序算法,它每次从未排序的部分选择最小(或最大)的元素,然后将其放置在已排序部分的末尾。边界条件在选择排序中是很重要的,因为它们决定了循环的范围。
在选择排序中,主要有两个循环:
-
外层循环:控制排序过程的迭代次数,从数组的第一个元素到倒数第二个元素(倒数第一个元素已经在内层循环中选择过了)。
-
内层循环:从外层循环的当前位置开始,遍历未排序部分,找到最小元素的索引。
基于上述描述,选择排序的边界条件如下:
-
外层循环的范围:从0到
n-2
,其中n
是数组的长度。这是因为当外层循环到达n-1
时,内层循环不需要再执行,因为最后一个元素已经是最大的。 -
内层循环的范围:从外层循环的当前位置
i
到n-1
,以寻找未排序部分中的最小元素。
以下是选择排序的Java代码示例,包括了边界条件的使用:
public static void selectionSort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n - 1; i++) { // 外层循环
int minIndex = i;
for (int j = i + 1; j < n; j++) { // 内层循环
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
// 交换 arr[i] 和 arr[minIndex]
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
注意,外层循环的终止条件是n - 1
,而内层循环的起始条件是外层循环的当前位置i + 1
。这些条件确保了循环的正确范围,使选择排序能够按预期工作。
快速排序
快速排序(Quick Sort)是一种高效的排序算法,它采用分治的思想来对一个序列进行排序。快速排序的基本思想是选择一个基准元素(通常是序列中的第一个元素),将序列中小于基准元素的值放在左边,大于基准元素的值放在右边,然后对左右两个子序列分别进行快速排序,最后合并得到有序序列。
快速排序的具体步骤如下:
- 选择基准元素(通常是序列中的第一个元素)。
- 将序列中小于基准元素的值移到基准元素的左边,大于基准元素的值移到基准元素的右边,同时基准元素所在的位置就是它在有序序列中的最终位置(称为划分操作)。
- 对基准元素左边的子序列和右边的子序列分别递归进行快速排序。
- 合并左右子序列,得到完整的有序序列。
下面是一个Python实现的快速排序代码:
def quick_sort(arr, first, last):
if first >= last:
return
low = first
high = last
pivot = arr[first] # 选择第一个元素作为基准
while low < high:
while low < high and arr[high] >= pivot:
high -= 1
arr[low] = arr[high]
while low < high and arr[low] < pivot:
low += 1
arr[high] = arr[low]
arr[low] = pivot
quick_sort(arr, first, low - 1) # 对基准左边的子序列进行快速排序
quick_sort(arr, low + 1, last) # 对基准右边的子序列进行快速排序
# 示例
arr = [9, 5, 3, 7, 2]
quick_sort(arr, 0, len(arr) - 1)
print(arr) # 输出结果为 [2, 3, 5, 7, 9]
在这个例子中,通过调用quick_sort
函数对列表arr
进行快速排序。初始时,first
为0,last
为len(arr)-1
,即整个列表的范围。在每次递归中,根据划分操作,确定了基准元素的位置,并将列表分成左右两个子序列进行递归排序,直到排序完成。
归并排序
归并排序(Merge Sort)也是一种常用的排序算法,同样采用分治的思想,将数组分成较小的子数组,然后对子数组进行排序,最后再将排序后的子数组合并成一个有序的数组。归并排序的基本思想如下:
- 将数组不断地对半分割,直到每个子数组只包含一个元素(可以认为每个只有一个元素的子数组是有序的)。
- 将相邻的两个子数组合并,合并后的子数组仍然保持有序,直至合并成一个完整的有序数组。
下面是归并排序的 Python 实现:
def merge_sort(arr):
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = arr[:mid] # 将数组分成左右两部分
right = arr[mid:]
left = merge_sort(left) # 递归地对左子数组进行排序
right = merge_sort(right) # 递归地对右子数组进行排序
return merge(left, right) # 合并排序后的左右子数组
def merge(left, right):
result = []
i, j = 0, 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.extend(left[i:])
result.extend(right[j:])
return result
归并排序的时间复杂度始终为 O(n log n),这使得它在大规模数据集上的性能相对较好。归并排序也是 Python 内置的排序函数 sorted()
和列表方法 sort()
的底层实现之一。
希尔排序
下面是希尔排序的Python实现:
def shell_sort(arr):
n = len(arr)
gap = n // 2
while gap > 0:
for i in range(gap, n):
temp = arr[i]
j = i
while j >= gap and arr[j - gap] > temp:
arr[j] = arr[j - gap]
j -= gap
arr[j] = temp
gap //= 2
# 示例
arr = [9, 5, 3, 7, 2, 8, 4, 1, 6]
shell_sort(arr)
print(arr) # 输出结果为 [1, 2, 3, 4, 5, 6, 7, 8, 9]
在这个例子中,我们实现了希尔排序的函数shell_sort
,并对一个包含9个元素的数组进行排序。首先,我们选择初始增量gap
为数组长度的一半,然后进行插入排序。在每一轮排序中,我们将当前元素与其前一个间隔gap
位置的元素进行比较,如果当前元素小于前一个间隔位置的元素,则将它们交换位置。然后,我们继续向前检查更远的元素,直到当前元素大于或等于前一个间隔位置的元素,或者已经到达数组的起始位置。最后,我们缩小增量gap
的值,并重复以上步骤,直到gap
为1,此时整个数组将会是有序的。
希尔排序在某些情况下可能会退化为冒泡排序。希尔排序的性能取决于步长序列的选择。如果选择的步长序列不合适,可能会导致排序性能下降,甚至退化为冒泡排序。
希尔排序的步骤是先将数组按照步长进行分组,然后对每个分组进行插入排序。随着步长的逐渐减小,分组数量减少,直到步长为1时,整个数组被分为一组,此时执行最后一次插入排序。
如果步长序列选择不当,比如过大或者步长递减得不够快,可能导致某些元素在一个分组中进行多次交换,这样就类似于冒泡排序的操作。这样希尔排序的性能就会退化为O(n^2)。
为了避免希尔排序退化为冒泡排序,通常需要选择合适的步长序列,比如Hibbard序列、Sedgewick序列等。这些步长序列可以保证希尔排序的性能优于冒泡排序。同时,也可以使用其他高效的排序算法来替代希尔排序,比如快速排序或归并排序,这样可以更好地保证排序的性能。
快排与归并排序的联系与区别
快速排序(Quick Sort)和归并排序(Merge Sort)是两种常见的排序算法,它们都属于分治法的应用。
联系:
- 都是基于分治法的思想,将原问题划分为子问题进行求解。
- 都使用递归来实现分治过程。
区别:
- 快速排序是一种原地排序算法,不需要额外的空间,而归并排序需要额外的空间来存储临时数组。
- 快速排序的核心操作是分区(Partition),通过选择一个基准元素将数组划分为两个子数组,左边的元素都小于基准,右边的元素都大于基准。而归并排序的核心操作是合并(Merge),将两个有序的子数组合并成一个有序的数组。
- 快速排序是一种不稳定的排序算法,即相等元素的相对顺序可能会发生改变,而归并排序是稳定的排序算法,相等元素的相对顺序保持不变。
- 快速排序的平均时间复杂度为O(nlogn),最坏情况下为O(n^2),而归并排序的时间复杂度始终为O(nlogn)。
- 快速排序通常在实践中比归并排序更快,因为它的内部循环可以更好地利用缓存性能。然而,在某些情况下,例如需要保证稳定性或者处理大规模数据时,归并排序可能更适合。
总体来说,快速排序和归并排序都是高效的排序算法,适用于不同的场景。快速排序在大多数情况下表现良好,而归并排序适用于需要稳定性和额外空间的场景。选择哪种算法取决于具体的需求和约束。
查找
对于大部分查找算法,首先要求待查找的数据是有序的。排序可以将数据按照一定的顺序排列,从而提供更好的查找性能。排序算法可以将数据集重新组织,使得查找过程更加高效。
在使用二分查找、插值查找、二叉搜索树等基于有序数据集的查找算法时,确保数据有序是必要的。因为这些算法都依赖于有序性,通过利用有序性,可以在每次比较后将搜索范围缩小一半,从而提高查找效率。
在应用这些查找算法之前,可以使用合适的排序算法(如快速排序、归并排序等)对数据进行排序。排序算法的选择取决于数据量、数据类型和性能要求等因素。
需要注意的是,有些查找算法(如哈希查找、布隆过滤器)不要求数据有序,因为它们通过哈希函数或位数组来快速定位目标元素,而不依赖于数据的顺序性。但是对于基于比较的查找算法,有序数据集是提高效率的重要前提。
以下是一些常见的查找算法:
-
线性查找(Linear Search):从头到尾逐个元素进行比较,直到找到目标元素或搜索完整个列表。
-
二分查找(Binary Search):适用于有序列表,通过将目标元素与列表中间的元素进行比较,来确定目标元素可能出现的位置。每次比较后,将搜索范围缩小一半,直到找到目标元素或确定不存在。
-
插值查找(Interpolation Search):类似于二分查找,但是在确定目标元素可能出现的位置时,使用了更好的估计方法。它根据目标元素与列表中最小值和最大值的比例来估计目标元素可能的位置,从而更快地收敛。
-
哈希查找(Hashing):使用哈希函数将关键字映射到存储位置,并在存储位置中查找目标元素。哈希查找具有快速的查找速度,但需要额外的空间来存储哈希表。
-
二叉搜索树(Binary Search Tree):通过构建一棵二叉搜索树,将列表中的元素按照一定的顺序进行存储。通过比较目标元素与当前节点的值,可以在树中快速定位目标元素。
-
平衡二叉搜索树(Balanced Binary Search Tree):在二叉搜索树的基础上进行优化,以保持树的平衡性,确保查找操作的效率始终保持在对数级别。
-
B树(B-Tree):一种多路搜索树,适用于大规模数据存储和高效查找的场景。B树具有平衡性和多路性,使得每次查找操作的比较次数较少。
-
布隆过滤器(Bloom Filter):一种概率型数据结构,用于判断一个元素是否可能存在于一个集合中。它使用多个哈希函数和位数组来表示元素的存在情况,具有高效的查找速度和低内存消耗。
这些算法在不同的场景和数据结构下具有不同的适用性和性能特点。根据具体的需求和数据特征,选择合适的查找算法可以提高查找效率和性能。
下面是几种常见查找算法的示例代码:
线性查找(Linear Search):
def linear_search(arr, target):
for i in range(len(arr)):
if arr[i] == target:
return i
return -1
# 示例
arr = [2, 5, 8, 12, 16, 23, 38, 56]
target = 16
result = linear_search(arr, target)
print(result) # 输出: 4
二分查找(Binary Search):
def binary_search(arr, target):
left = 0
right = len(arr) - 1
while left <= right:
mid = (left + right) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
# 示例
arr = [2, 5, 8, 12, 16, 23, 38, 56]
target = 16
result = binary_search(arr, target)
print(result) # 输出: 4
哈希查找(Hashing):
def hash_search(hash_table, target):
hash_value = hash(target)
if hash_value in hash_table:
return hash_table[hash_value]
else:
return None
# 示例
hash_table = {hash("apple"): "fruit", hash("banana"): "fruit", hash("carrot"): "vegetable"}
target = "apple"
result = hash_search(hash_table, target)
print(result) # 输出: "fruit"
这些示例代码演示了不同的查找算法,并给出了具体的示例用法。根据需要选择适合的算法,并将其应用于实际场景中的数据结构。