1.排序定义
排序算法是一种将数据元素按照特定顺序排列的算法,通常用于将一组无序的数据(如数字、字符串等)重新组织成有序的形式。排序的顺序可以是升序(从小到大)或降序(从大到小)。排序算法在计算机科学中有很多应用,比如在搜索、数据分析和数据库管理等方面。
在分析排序算法时,时间复杂度、空间复杂度和稳定性是三个重要的概念。下面是它们的定义和解释:
2.衡量指标
2.1时间复杂度
时间复杂度是指算法执行所需时间与输入数据规模之间的关系。它通常用大O符号表示,描述了最坏情况、平均情况和最佳情况的执行时间。常见的时间复杂度有:
-
O(1):常数时间,算法执行时间与输入数据规模无关。
-
O(log n):对数时间,通常出现在分治算法中。
-
O(n):线性时间,算法执行时间与输入数据规模成正比。
-
O(n log n):线性对数时间,常见于高效排序算法,如归并排序和快速排序。
-
O(n²):平方时间,常见于简单的排序算法,如冒泡排序、选择排序和插入排序。
2. 2空间复杂度
空间复杂度是指算法在执行过程中所需内存空间与输入数据规模之间的关系。同样,空间复杂度也用大O符号表示。常见的空间复杂度有:
-
O(1):常数空间,算法只需要固定数量的额外空间。
-
O(n):线性空间,算法需要与输入数据规模成正比的额外空间。
-
O(n²):平方空间,算法需要的额外空间与输入规模的平方成正比。
2. 3稳定性
稳定性是指在排序过程中,如果两个相等的元素相对位置不变,则该排序算法是稳定的。也就是说,稳定的排序算法在排序后,相等元素的相对顺序与排序前保持一致。稳定性在某些情况下非常重要,尤其是在需要多次排序时。
总结
-
时间复杂度 主要衡量算法效率。
-
空间复杂度 关注算法的内存使用。
-
稳定性 影响排序后的元素顺序,特别是在多次排序时。
在选择排序算法时,通常需要综合考虑这些因素,以满足具体应用的需求。
3.算法分类
常见的算法共有四大类、七种,其相应关系可见下图。
3.1插入排序
3.1.1直接插入排序
直接插入排序是一种简单的排序算法,其基本思路是将数据分为已排序和未排序两部分。算法通过逐个将未排序部分的元素插入到已排序部分的正确位置来实现排序。具体步骤如下:
1. 从第二个元素开始,认为第一个元素已经排好序。
2. 取出当前元素(称为“待插入元素”),并与已排序部分的元素进行比较。
3. 将已排序部分中大于待插入元素的元素向右移动一位,为待插入元素腾出位置。
4. 将待插入元素放入正确的位置。
5. 重复以上步骤,直到所有元素都被插入到已排序部分,最终得到一个有序的数组。
直接插入排序的时间复杂度为 O(n²),最坏情况下需要进行 n(n-1)/2 次比较和移动,适合于小规模数据或部分有序的数组。它的空间复杂度为 O(1),因为只需要常数级的额外空间。该算法是稳定的,因为相等的元素在排序后保持相对位置不变。
public void insertSort(int[] array) {
//直接插入排序
//思想:前面的数据看作有序的,依次遍历后面的数据群,将其插入到前面的数据群中去,使得前面的数据群变为有序
//当所有的乱序元素遍历完时,整个数组直接插入排序完成。
for (int i = 1; i < array.length; i++) {
//比较乱序数组的第一个元素与有序数组的每个元素,直到下标为0为止。
int tmp = array[i];
//指向乱序的数组下标与指向有序数组下标是有关联的,所以用一个变量表示即可
int j = i - 1;
for (; j >= 0; j--) {
//当数组改变时,其对应下标的值也会改变
//当乱序的元素比有序的元素的值小时
if (tmp < array[j]) {
array[j + 1] = array[j];
//此时还需判断一下
/* if(j ==0){
array[j] =tmp;
sortend++;
}*/
} else {
array[j + 1] = tmp;
break;
}
}
//当执行到此时,j 是0还是-1?是 - 1
//当乱序的元素插入到有序元素的中间时,这条语句的执行会不会出现错误?
//此种情况下j不为-1.
array[j + 1] = tmp;
}
}
3.1.2希尔排序
希尔排序是一种基于插入排序的排序算法,其基本思路是通过将原始数据分成多个子序列,以便对这些子序列进行局部排序,然后逐渐减小子序列的间隔,最终实现整体排序。具体步骤如下:
-
选择间隔:首先确定一个间隔(通常称为“增量”),将待排序的数组划分为多个子序列。初始时,增量可以是数组长度的一半。
-
分组排序:根据当前的增量,将数组分为若干个子序列,然后对每个子序列进行插入排序。插入排序可以在每个子序列内部进行,确保每个子序列都是有序的。
-
缩小间隔:逐步减小增量,通常将增量减半,直到增量为 1。这意味着所有元素都会被纳入一个子序列中。
-
最终排序:当增量为 1 时,对整个数组执行一次插入排序,确保所有元素都是有序的。
希尔排序的优点在于,它通过在初始阶段对数据进行局部排序,减少了后续插入排序时的移动次数,从而提高了效率。其时间复杂度并不固定,取决于增量的选择,最坏情况下为 O(n²),而在最佳情况下可以达到 O(nlogn)或更优,空间复杂度为 O(1)。希尔排序不保证稳定性,因为在分组排序过程中,相同元素的相对位置可能会发生变化。
public void shellsort(int[] array) {
//希尔排序
//将每次分组后的数据进行调整,根据gap进行分组
for (int gap = array.length / 2; gap >= 1; gap = gap / 2) {
//当gap ==1时,每一个元素会不会与前面的元素进行比较?
//当某一个元素在两个分组中,自
shell(array, gap);
}
}
private void shell(int[] array, int gap) {
for (int i = gap; i < array.length; i++) {
//比较乱序数组的第一个元素与有序数组的每个元素,直到下标为0为止。
int tmp = array[i];
//指向乱序的数组下标与指向有序数组下标是有关联的,所以用一个变量表示即可
int j = i - gap;
for (; j >= 0; j-=gap) {
//当数组改变时,其对应下标的值也会改变
//当乱序的元素比有序的元素的值小时
if (tmp < array[j]) {
array[j + gap] = array[j];
} else {
array[j + gap] = tmp;
break;
}
}
array[j + gap] = tmp;
}
}
3.2选择排序
3.2.1直接选择排序
直接选择排序是一种简单的排序算法,其基本思路是通过反复选择未排序部分中的最小(或最大)元素,并将其放到已排序部分的末尾。具体步骤如下:
-
初始状态:将待排序的数组看作两个部分:已排序部分和未排序部分。最开始,已排序部分为空,未排序部分包含整个数组。
-
选择最小元素:在未排序部分中找到最小的元素。
-
交换位置:将找到的最小元素与未排序部分的第一个元素进行交换,这样最小元素就被放到了已排序部分的末尾。
-
更新部分:已排序部分增加一个元素,未排序部分减少一个元素。
-
重复操作:重复以上步骤,直到未排序部分的元素全部被放入已排序部分,整个数组完成排序。
直接选择排序的时间复杂度为 O(n²),在最坏和平均情况下都需要进行 n(n−1)/2 次比较。虽然它的性能不如其他高效排序算法,但它的空间复杂度为 O(1),因为只需要常数级的额外空间,并且它的实现比较简单。此外,直接选择排序是不稳定的,因为在交换过程中,相等元素的相对位置可能会发生变化。
public void selectSort1(int[] array) {
//每次挑选出一个最小的数据,放在小下标处,总共挑选array.length-1次
for (int k = 0; k < array.length-1; k++) {
// 不存储数据,存储下标
int tmp =k; //令最小的数据的下标为tmp
for (int i = k+1; i < array.length; i++) {
if (array[i] < array[tmp]) {
tmp = i;
}
}
swap(array,tmp,k);
}
}
private void swap(int[] array,int x,int y){
int tmp = array[x];
array[x] =array[y];
array[y] =tmp;
}
3.2.2堆排序
堆排序是一种基于堆数据结构的排序算法,其基本思路是利用堆的性质将数组排序。堆是一种完全二叉树,通常分为最大堆和最小堆。在最大堆中,父节点的值总是大于或等于其子节点的值,而在最小堆中,父节点的值总是小于或等于其子节点的值。堆排序的步骤如下:
-
构建最大堆:将输入的无序数组转化为一个最大堆。最大堆的性质是每个父节点的值都大于或等于其子节点的值。
-
交换和调整:将最大堆的根节点(即最大值)与数组的最后一个元素交换。然后,将剩余的部分重新调整为最大堆。
-
重复操作:对数组的剩余部分重复步骤2,直到所有元素都被排序。
具体步骤可以细分为:
-
初始化:从数组中找到最后一个非叶子节点,然后从这个节点开始向上调整堆结构。
-
堆调整:对于每一个节点,确保其值大于或等于其子节点的值,必要时交换节点与子节点的值,并继续调整子节点。
-
排序过程:每次取出最大堆的根节点(最大值),将其放到数组的末尾,然后调整剩余的堆。
堆排序的时间复杂度为 O(nlogn),其中构建最大堆的时间复杂度为 O(n),而每次调整堆的时间复杂度为 O(logn)。空间复杂度为 O(1),因为堆排序是原地排序算法。需要注意的是,堆排序是一种不稳定的排序算法,相同元素的相对顺序可能会发生变化。
public class HeapSort {
// 堆排序的主方法
public void heapSort(int[] array) {
// 首先构建最大堆
for (int k = array.length / 2 - 1; k >= 0; k--) {
heapify(array, array.length, k);
}
// 逐个提取元素
for (int k = array.length - 1; k > 0; k--) {
swap(array, 0, k); // 将堆顶元素(最大值)与当前最后一个元素交换
heapify(array, k, 0); // 调整堆
}
}
// 将以index为根的子树调整为最大堆
private void heapify(int[] array, int n, int index) {
int largest = index; // 初始化最大值为根节点
int left = 2 * index + 1; // 左子节点
int right = 2 * index + 2; // 右子节点
// 如果左子节点存在且比根节点大
if (left < n && array[left] > array[largest]) {
largest = left;
}
// 如果右子节点存在且比当前最大值大
if (right < n && array[right] > array[largest]) {
largest = right;
}
// 如果最大值不是根节点,交换并递归调整
if (largest != index) {
swap(array, largest, index);
heapify(array, n, largest);
}
}
// 交换数组中的两个元素
private void swap(int[] array, int x, int y) {
int tmp = array[x];
array[x] = array[y];
array[y] = tmp;
}
3.3交换排序
3.3.1冒泡排序
冒泡排序是一种简单的排序算法,其基本思路是通过反复比较相邻的元素,将较大的元素“冒泡”到数组的末尾。具体步骤如下:
-
初始状态:将待排序的数组看作一个未排序的序列,开始时整个序列都是未排序的。
-
相邻比较:从数组的第一个元素开始,依次比较相邻的两个元素。如果前一个元素大于后一个元素,就交换它们的位置。
-
冒泡过程:每一轮比较结束后,当前未排序部分中的最大元素会“冒泡”到数组的末尾。这样,每次遍历后,最大的元素就会放在已排序部分的最后。
-
重复操作:对未排序的部分重复上述相邻比较和交换的过程,直到没有元素需要交换为止。此时,数组已完成排序。
-
结束条件:可以设置一个标志位来检查在某一轮遍历中是否发生了交换,如果没有发生交换,则说明数组已排序,可以提前结束算法。
冒泡排序的时间复杂度为 O(n²),其中在最坏和平均情况下需要进行 n(n−1)/2次比较。而在最好情况下(当数组已经有序时),时间复杂度为 O(n)。空间复杂度为 O(1),因为冒泡排序是原地排序算法,只需要常数级的额外空间。此外,冒泡排序是一种稳定的排序算法,相同元素的相对顺序在排序过程中不会发生变化。
public void bubblesort(int[] array){
//冒号排序每次比较出一个最大的值
for (int i = 0; i <array.length-1 ; i++) {
//对冒号排序进行优化,在冒号排序过程中,有可能一轮比较已经达到有序
boolean fig = true;
for (int j = 0; j < array.length-i-1; j++) {
if(array[j]>array[j+1]){
swap(array,j,j+1);
fig =false;
}
}
if(fig ==true){
break;
}
}
}
3.3.2快速排序
快速排序是一种高效的排序算法,其基本思路是采用分治法(Divide and Conquer)将待排序数组划分为更小的子数组,然后递归地对这些子数组进行排序。具体步骤如下:
-
选择基准:从数组中选择一个元素作为“基准”(pivot)。选择基准的方法可以有多种,比如选择第一个元素、最后一个元素或随机选择。
-
划分操作:将数组重新排列,使得所有小于基准的元素都在基准的左侧,所有大于基准的元素都在基准的右侧。此时,基准元素的位置是已排序的。
-
递归排序:递归地对基准左侧和右侧的子数组进行快速排序,直到子数组的大小为1或0,此时数组已经排序完成。
-
结束条件:递归终止条件是子数组的大小为0或1,因为这些情况已经是有序的。
快速排序的时间复杂度为 O(n²),最坏情况下(例如,已经有序的数组)需要进行 n² 次比较,而平均情况下,时间复杂度为 O(nlogn)。空间复杂度为 O(logn),主要由递归调用栈的深度决定。快速排序是原地排序算法,但在某些情况下可能需要额外的存储空间。此外,快速排序是一种不稳定的排序算法,相同元素的相对顺序在排序过程中可能会发生变化。
public void quicksort(int []array){
quickwakeng(array,0,array.length-1);
//quickhore(array,0,array.length-1);
}
private void quickwakeng(int []array,int start,int end) {
if (start >= end) {
return;
}
int starttmp = start;
int tmp = array[start];
while (start < end) {
while (array[end] >= tmp && start < end) {
end--;
}
//可以用当前start与end标记当前停止指向的位置,来表示空位置
array[start] = array[end];
//此时end所指向的空间为空,
while (array[start] <= tmp && start < end) {
start++;
}
array[end] = array[start];
}
array[start] = tmp ;
quickwakeng(array,starttmp,start-1); //start的值一定为0吗?不一定
quickwakeng(array,start+1,array.length-1); //end的值不一定为array.length-1,但是递归时,end不会局限array.lenth-1
}
3.4归并排序
归并排序是一种基于分治法的高效排序算法,其基本思路是将待排序数组分为两个子数组,分别对这两个子数组进行排序,然后将已排序的子数组合并成一个有序数组。具体步骤如下:
-
分割操作:将待排序的数组递归地分为两半,直到每个子数组只包含一个元素(因为一个元素本身是有序的)。
-
合并操作:将两个已排序的子数组合并成一个有序数组。在合并过程中,比较两个子数组的元素,依次将较小的元素放入新的数组中,直到一个子数组的所有元素都被合并。
-
递归过程:递归进行上述的分割和合并操作,直到整个数组排序完成。
-
结束条件:递归终止条件是子数组的大小为1,此时数组已经是有序的。
归并排序的时间复杂度为 O(nlog n),因为每次分割操作的复杂度是 O(logn),而合并操作的复杂度为 O(n)。空间复杂度为O(n),因为在合并过程中需要使用额外的数组来存放合并后的结果。此外,归并排序是一种稳定的排序算法,相同元素的相对顺序在排序过程中不会发生变化。
//归并排序递归实现
private void recursionincorporate(int []array,int left,int right){
//如果划分的数组宽度只有一个元素大小时,则返回
if(left>=right){
return;
}
//获取中间下标
int mid = (left+right)/2;
//划分数组
recursionincorporate(array,left,mid);
recursionincorporate(array,mid+1,right);
//划分数组完成后:开始进行合并,进行直接插入排序,
//这样岂不是多此一举,合并时不应该采用直接插入排序
//insertRangeSort(array,left,right);
merge(array,left,right,mid);
//排序完成后,返回上一个函数
}
private void merge(int []array,int left,int right,int mid){
//将有序的数据先存放到临时的数组中去
int[] tmp = new int[right-left+1];
//对指定区间内两组数据进行排序
int s1 = left;
int e1 = mid;
int s2 = mid+1;
int e2 = right;
int k = 0;
while (s1<=e1&&s2<=e2){
if(array[s1]<=array[s2]){
tmp[k++] = array[s1++]; //如何设置临时数组的下标?我们需要循环一次,便++的下标,设置即可
}else {
tmp[k++] = array[s2++];
}
}
//当退出循环时,说明有一个数组已经赋值完毕
while (s1<=e1){
//此时说明s2传送完毕
tmp[k++] = array[s1++];
}
while (s2<=e2){
tmp[k++] = array[s2++];
}
//将所有的有序数据存储到临时数组中后
//赋值给array数组
// right是数组的下标范围,还不能表示临时数组中元素的个数。
// for (int i = 0; i <=right; i++) {
for (int i = 0; i <k; i++) {
array[i+left] = tmp[i];
}
}
4.总结
直接插入排序 | 希尔排序 | 直接选择排序 | 堆排序 | 冒泡排序 | 快速排序 | 归并排序 | |
时间复杂度 | O(n²) | O(nlogn) ~O(n²) | O(n²) | O(nlogn) | O(n²) | O(nlogn) | O(nlogn) |
空间复杂度 | O(1) | O(1) | O(1) | O(1) | O(1) | O(logn) | O(n) |
稳定性 | 稳定 | 不稳定 | 不稳定 | 不稳定 | 稳定 | 不稳定 | 稳定 |