【数据结构】8 种常见排序的总结(插入\希尔\选择\归并\冒泡\快速\堆\计数)含动图


排序算法是计算机科学中的一个重要主题,它可以使数据按照某种顺序排列。排序算法的稳定性、性能以及时间和空间复杂度是评估算法优劣的关键因素。下面将详细介绍8种经典排序算法的稳定性、性能以及时间和空间复杂度,以及应用场景分析。

1. 八种排序的介绍与实现

1.1.插入排序

插入排序是一种简单的排序算法,它将数据分为已排序和未排序两个部分,每次从未排序部分取出一个元素,并将其插入到已排序部分的合适位置。插入排序的稳定性是很好的,它对于相等的元素是保持原来的顺序不变。插入排序的时间复杂度为 O(n^2)空间复杂度为 O(1),在数据规模较小的情况下表现良好。请添加图片描述

/*
   InsertSort - 插入排序算法
   @param a 整数类型数组,表示待排序的数组
   @param n 整数类型变量,表示数组a的长度
*/
void InsertSort(int* a, int n) {
    for (int i = 0; i < n - 1; i++) {
        // 写排序的时候先写单趟
        int end = i;
        int tmp = a[end + 1];

        // 在已排序区间中查找插入点
        while (end >= 0) {
            if (tmp < a[end]) { // 需要将tmp插入当前位置之前
                a[end + 1] = a[end]; // 将当前位置之前的元素后移
            } else {
                break;
            }
            --end;
        }

        a[end + 1] = tmp; // 将tmp插入到已排序区间中
    }
}

C++ 版本:

class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        for (int i = 0; i < nums.size(); ++i)
        {
            for (int j = i; j > 0; --j)
            {
                if (nums[j-1] > nums[j]) 
                {
                    swap(nums[j], nums[j-1]);
                }
            }
        }
        return nums;
    }
};

1.2. 希尔排序

希尔排序的平均时间复杂度通常被认为是介于O(n)和O(n ^ 2)之间,具体取决于选取的间隔序列。在实际应用中,常用的希尔排序的时间复杂度为O(n^1.3),这是基于经验得出的一个较好的估计。但需要注意的是,希尔排序的时间复杂度并不是一个确定的值,而是与输入数据的特性和选取的间隔序列有关。

希尔排序之所以比简单的插入排序性能更好,是因为它通过多次插入排序的过程来减小乱序元素之间的距离,使得每次插入排序时,元素移动的次数较少。虽然无法给出希尔排序的准确时间复杂度,但在大多数情况下,希尔排序都能够提供较快的排序速度。

总结而言,希尔排序的时间复杂度是在O(n)和O(n ^ 2)之间,根据经验通常可以估计为O(n^1.3)。但需要明确的是,希尔排序的性能受到选取的间隔序列以及输入数据的影响,因此在具体应用中,可能需要根据实际情况进行评估和调整。

希尔排序的稳定性较差,即相等元素的顺序可能会被改变,空间复杂度为 O(1)。在数据规模较大时表现良好。请添加图片描述

/* 
   ShellSort - 希尔排序算法
   @param a 整数类型数组,表示待排序的数组
   @param n 整数类型变量,表示数组a的长度
*/
void ShellSort(int* a, int n) {
    int gap = n; // 初始化间隔数

    while (gap > 1) { // 只要间隔数大于1就继续排序
        gap = gap / 3 + 1; // 通过公式计算间隔数
        for (int i = 0; i < n - gap; i++) { // 按照间隔数分组排序
            int end = i; // end代表当前组的最后一个元素
            int tmp = a[end + gap]; // tmp保存需要插入到已排序区间中的元素

            // 在已排序区间中查找插入点
            while (end >= 0) {
                if (tmp < a[end]) { // 需要将tmp插入当前位置之前
                    a[end + gap] = a[end]; // 将当前位置之前的元素后移
                    end -= gap; // 继续向前查找
                } else { // 已经找到插入点,退出循环
                    break;
                }
            }
            a[end + gap] = tmp; // 将tmp插入到已排序区间中
        }
    }
}

C++ 版本:

class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        int n = nums.size();
        int gap = n;
        while (gap > 1)
        {
            gap = gap / 3 + 1;
            for (int i = gap; i < n; i += gap) {
                for (int j = i; j > 0; j -= gap) {
                    if (nums[j] < nums[j-gap]) swap(nums[j], nums[j-gap]);
                }
            }
        }
        return nums;
    }
};

1.3. 选择排序

选择排序是一种简单的排序算法,它每次从未排序部分选择最小的元素并将其放到已排序部分的末尾。选择排序的稳定性很差,即相等元素的顺序可能会被改变。选择排序的时间复杂度为 O(n^2)空间复杂度为 O(1)请添加图片描述

/*
   SelectSort - 选择排序算法
   @param a 整数类型数组,表示待排序的数组
   @param n 整数类型变量,表示数组a的长度
*/
void SelectSort(int* a, int n)
{
    int maxi = 0; // 记录当前无序区间中的最大值下标
    int mini = 0; // 记录当前无序区间中的最小值下标
    int begin = 0; // 记录无序区间的起始下标
    int end = n - 1; // 记录无序区间的结束下标

    while (begin < end)
    {
        // 在无序区间内查找最大值和最小值的下标
        for (int i = begin; i <= end; i++)
        {
            if (a[maxi] < a[i]) // 更新最大值下标
            {
                maxi = i;
            }
            if (a[mini] > a[i]) // 更新最小值下标
            {
                mini = i;
            }
        }

        Swap(&a[begin], &a[mini]); // 将无序区间中的最小值放到有序区间的末尾
        if (maxi == begin) { // 如果最大值下标为begin,则需要更新
            maxi = mini;
        }
        Swap(&a[end], &a[maxi]); // 将无序区间中的最大值放到有序区间的开头

        --end; // 结束下标向前移动
        ++begin; // 起始下标向后移动
    }
}

C++版本:

class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        int n = nums.size();
        for (int i = 0; i < n; ++i)
        {
            int minIndex = i;
            for (int j = i; j < n; ++j) 
            {
                if (nums[minIndex] > nums[j]) minIndex = j;
            }
            swap(nums[i], nums[minIndex]);
        }
        return nums;
    }
};

1.4.归并排序

归并排序是一种分治算法,它将数据分为两个子序列并对每个子序列进行排序,最后将两个有序子序列合并成一个有序序列。归并排序的稳定性很好,即相等元素的顺序会被保持不变。归并排序的时间复杂度为 O(nlogn)空间复杂度为 O(n)请添加图片描述

// 归并排序递归实现
void _MergeSort(int* a, int* tmp, int left, int right)
{
    if (right <= left)
        return;

    int mid = (right + left) / 2;
    _MergeSort(a, tmp, left, mid);          // 对左半部分进行归并排序
    _MergeSort(a, tmp, mid + 1, right);     // 对右半部分进行归并排序

    // 归并到tmp数组,再拷贝回去
    int left1 = left, right1 = mid;        // 左半部分的起始和终止位置
    int left2 = mid + 1, right2 = right;   // 右半部分的起始和终止位置
    int pos = left;                        // 归并后结果存放的位置
    while (left1 <= right1 && left2 <= right2)
    {
        if (a[left1] < a[left2])
        {
            tmp[pos++] = a[left1++];       // 将较小的元素存入tmp数组
        }
        else
        {
            tmp[pos++] = a[left2++];
        }
    }

    while (left1 <= right1)
    {
        tmp[pos++] = a[left1++];           // 将剩余的左半部分元素存入tmp数组
    }

    while (left2 <= right2)
    {
        tmp[pos++] = a[left2++];           // 将剩余的右半部分元素存入tmp数组
    }

    memcpy(a + left, tmp + left, (right - left + 1) * sizeof(int));   // 将tmp数组中排序好的部分拷贝回原数组a
}

void MergeSort(int* a, int n)
{
    int* tmp = (int*)malloc(sizeof(int) * (n + 1));
    _MergeSort(a, tmp, 0, n);
    free(tmp);
}

// 归并排序非递归实现
void MergeSortNonR(int* a, int n)
{
    int gap = 1;                       // 初始间隔为1
    int* tmp = (int*)malloc(sizeof(int) * (n + 1));

    while (gap < n)
    {
        for (int i = 0; i < n; i += 2 * gap)   // 每次处理两个相邻的子序列
        {
            int left1 = i, right1 = i + gap - 1;
            int left2 = i + gap, right2 = i + 2 * gap - 1;

            int pos = i;                   // 归并后结果存放的位置

			// 防止数组越界
            if (left2 >= n)
            {
                break;
            }

            if (right2 >= n)
            {
                right2 = n - 1;
            }

            while (left1 <= right1 && left2 <= right2)
            {
                if (a[left1] < a[left2])
                {
                    tmp[pos++] = a[left1++];     // 将较小的元素存入tmp数组
                }
                else
                {
                    tmp[pos++] = a[left2++];
                }
            }

            while (left1 <= right1)
            {
                tmp[pos++] = a[left1++];        // 将剩余的左半部分元素存入tmp数组
            }

            while (left2 <= right2)
            {
                tmp[pos++] = a[left2++];        // 将剩余的右半部分元素存入tmp数组
            }

            memcpy(a + i, tmp + i, (2 * gap) * sizeof(int));   // 将tmp数组中排序好的部分拷贝回原数组a
        }
        gap *= 2;                           // 间隔扩大两倍
    }

    free(tmp);
}

1.5. 冒泡排序

冒泡排序是一种简单的排序算法,它通过重复地交换相邻元素将未排序部分的最大元素“冒泡”到已排序部分的末尾。冒泡排序的稳定性很好,即相等元素的顺序会被保持不变。冒泡排序的时间复杂度为 O(n^2)空间复杂度为 O(1)
请添加图片描述

// 冒泡排序
void BubbleSort(int* a, int n)
{
    for (int i = 0; i < n; i++) // 外层循环控制比较轮数
    {
        for (int j = 1; j < n - i; j++) // 内层循环从第一个元素开始,依次比较相邻的两个元素
        {
            if (a[j - 1] > a[j]) // 如果前一个元素大于后一个元素,则交换它们的位置
            {
                Swap(&a[j - 1], &a[j]);
            }
        }
    }
}

C++版本:

class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        int n = nums.size();
        for (int i = 0; i < n; ++i) {
            for (int j = 1; j < n-i; ++j) {
                if (nums[j-1] > nums[j]) swap(nums[j-1], nums[j]);
            }
        }
        return nums;
    }
};

1.6. 快速排序

快速排序是一种分治算法,它通过选取一个基准元素将数据分为两个子序列,并对每个子序列进行排序。快速排序通常明显快于其他排序算法,但对于相等元素的顺序可能会被改变,稳定性较差,它的时间复杂度为 O(nlogn)空间复杂度为 O(logn)

hoare法

请添加图片描述

挖坑法

请添加图片描述

前后指针法

请添加图片描述

用栈实现非递归

请添加图片描述

// 获取中间元素作为枢纽值,并对 arr[left]、arr[mid]、arr[right] 进行排序
void getPivot(int arr[], int left, int right) 
{
    int mid = left + (right - left) / 2;

    // 将 arr[left]、arr[mid]、arr[right] 进行排序
    if (arr[left] > arr[mid])
        Swap(&arr[left], &arr[mid]);
    if (arr[left] > arr[right])
        Swap(&arr[left], &arr[right]);
    if (arr[mid] > arr[right])
        Swap(&arr[mid], &arr[right]);

    Swap(&arr[mid], &arr[left]);
}

// 快速排序递归实现(Hoare版本)
int PartSort1(int* a, int left, int right)
{
    getPivot(a, left, right); // 获取枢纽值

    int keyi = left; // 枢纽值索引
    while (left < right)
    {
        // 找比枢纽值小的元素
        while (a[right] >= a[keyi] && left < right)
        {
            right--;
        }
        // 找比枢纽值大的元素
        while (a[left] <= a[keyi] && left < right)
        {
            left++;
        }
        // 交换找到的两个元素
        Swap(&a[left], &a[right]);
    }
    Swap(&a[left], &a[keyi]); // 将枢纽值放到正确的位置上
    return left; // 返回枢纽值的索引
}

// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
    // getPivot(a, left, right); // 获取枢纽值(在该方法中未使用)

    int key = a[left]; // 枢纽值
    int hole = left; // 坑的位置
    while (left < right)
    {
        // 找比枢纽值小的元素
        while (a[right] >= key && left < right)
        {
            right--;
        }
        a[hole] = a[right]; // 将较小的元素填到坑中
        hole = right;
        // 找比枢纽值大的元素
        while (a[left] <= key && left < right)
        {
            left++;
        }
        a[hole] = a[left]; // 将较大的元素填到坑中
        hole = left;
    }
    a[hole] = key; // 将枢纽值放到坑中
    return hole; // 返回枢纽值的位置
}

// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
    getPivot(a, left, right); // 获取枢纽值

    int keyi = left; // 枢纽值索引
    int cur = left + 1; // 当前指针
    int prev = left; // 前一个指针

    while (cur <= right)
    {
        if (a[cur] < a[keyi] && ++prev != cur)
        {
            Swap(&a[prev], &a[cur]);
        }
        ++cur;
    }
    Swap(&a[left], &a[prev]); // 将枢纽值放到正确的位置上
    return prev; // 返回枢纽值的索引
}

// 快速排序递归实现
void QuickSort(int* a, int left, int right)
{
    if (left >= right)
        return;
    int keyi = PartSort3(a, left, right); // 获取枢纽值位置

    QuickSort(a, left, keyi - 1); // 对左边部分进行快速排序
    QuickSort(a, keyi + 1, right); // 对右边部分进行快速排序
}

// 快速排序非递归实现
void QuickSortNonR(int* a, int left, int right)
{
    Stack st;
    StackInit(&st); // 初始化栈

    StackPush(&st, left);
    StackPush(&st, right);

    while (!StackEmpty(&st))
    {
        right = StackTop(&st);
        StackPop(&st);
        left = StackTop(&st);
        StackPop(&st);

        if (right - left <= 1)
            continue;

        int div = PartSort1(a, left, right); // 获取枢纽值位置

        StackPush(&st, left);
        StackPush(&st, div);

        StackPush(&st, div + 1);
        StackPush(&st, right);
    }

    StackDestroy(&st); // 销毁栈
}

1.7.堆排序

堆排序是一种通过维护一个二叉堆来实现的排序算法,它将未排序部分的最大元素与已排序部分的末尾交换,然后重建堆,稳定性较差。堆排序的时间复杂度为 O(nlogn)空间复杂度为 O(1),但对于相等元素的顺序可能会被改变。请添加图片描述

// 向下调整算法,使以 parent 为根节点的堆满足大根堆性质
void AdjustDown(int* a, int parent, int n)
{
	assert(a);
	int child = parent * 2 + 1;

	// 确保子节点不超过堆的大小
	while (child < n)
	{
		// 找到左右子节点中较大的一个
		if (child + 1 < n && a[child] < a[child + 1])
		{
			++child;
		}

		// 父节点小于较大子节点,交换父子节点位置
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break; // 父节点已经大于等于子节点,退出循环
		}
	}
}

// 堆排序算法
void HeapSort(int* a, int n)
{
	// 升序排序建大根堆,降序排序建小根堆
	for (int i = (n - 1) / 2; i >= 0; i--) // 从最后一个非叶子节点开始向下调整
	{
		AdjustDown(a, i, n); // 向下调整以 i 为根节点的大根堆
	}

	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]); // 将堆顶元素(即最大值)与堆末尾元素交换
		AdjustDown(a, 0, end); // 对新的堆顶进行向下调整,使其满足大根堆性质
		--end; // 堆大小减 1,排除已排序好的最大值
	}
}

1.8. 计数排序

计数排序是一种非比较的排序算法,它的思想是统计每个元素在数组中出现的次数,并根据元素出现的次数依次输出元素。计数排序的稳定性很不好,就没有顺序可言。计数排序的时间复杂度为 O(n+k),其中,k是待排序数组中最大元素的值空间复杂度也是 O(n+k)

// 计数排序算法
void CountSort(int* a, int n)
{
	// 找到数组中的最小值和最大值
	int min = a[0], max = a[0];
	for (int i = 0; i < n; i++)
	{
		if (a[i] < min)
			min = a[i];
		if (a[i] > max)
			max = a[i];
	}

	// 计算数值范围
	int range = max - min + 1;

	// 创建并初始化计数数组
	int* count = (int*)malloc(sizeof(int) * range);
	memset(count, 0, sizeof(int) * range);

	// 统计每个数值出现的次数
	for (int i = 0; i < n; i++)
	{
		count[a[i] - min]++;
	}

	// 根据计数数组重新填充原始数组
	int pos = 0;
	for (int i = 0; i < range; i++)
	{
		while (count[i]--)
		{
			a[pos++] = i + min;
		}
	}
}

2. 八种排序的应用场景分析

2.1. 插入排序:

  • 相对好情况:当待排序数组的规模较小或已经部分有序时,插入排序具有较好的性能;
  • 最适用场景:适用于小规模或基本有序的数据集合,实现简单,对于部分有序的情况有良好的性能。
  • 不适合使用的场景:当待排序数组规模非常大时,插入排序的时间复杂度为O(n^2),性能较差,在这种情况下可能不适合使用插入排序。

2.2. 希尔排序:

  • 相对好情况:当待排序数组规模较大且分布较均匀时,希尔排序能够提供相对高效的排序;
  • 最适用场景:适用于中等规模的数据集合,但对于较大规模数据排序时,也可以考虑使用希尔排序。
  • 不适合使用的场景:尽管希尔排序对于中等规模数据集合有良好的性能,但在数据集合规模非常大的情况下,希尔排序可能变得相对缓慢,不如其他更高效的排序算法。

2.3. 选择排序:

  • 相对好情况:由于选择排序的交换操作较少,因此在数据移动成本较高的场景下可能有一些优势;
  • 最适用场景:适用于小规模数据集合,特别是在空间复杂度有限或数据移动成本较高的情况下。
  • 不适合使用的场景:由于选择排序的时间复杂度为O(n^2),不论输入数据的分布情况,它都需要进行大量的比较和交换操作,因此在较大规模的数据集合上通常不适合使用。

2.4. 归并排序:

  • 相对好情况:归并排序在各种情况下都能提供稳定的O(nlogn)的时间复杂度,因此对于大规模数据集合是一种较为优秀的选择;
  • 最适用场景:适用于对大规模数据集合进行排序,尤其在需要稳定排序和合并操作的场景下效果明显。
  • 不适合使用的场景:归并排序的主要缺点是需要额外的空间来存储归并过程中的临时数组,因此在对空间要求较为严格的场景下,不适合使用归并排序。此外,归并排序的时间复杂度较高,为O(nlogn),虽然在大规模数据集合上表现良好,但对于小规模数据集合可能存在性能不足的问题。

2.5. 冒泡排序:

  • 相对好情况:当待排序数组已经基本有序时,冒泡排序的比较次数会减少,因此在这种情况下性能相对好;
  • 最适用场景:适用于小规模或基本有序的数据集合,但由于其较高的时间复杂度,通常不适用于大规模数据排序。
  • 不适合使用的场景:冒泡排序的时间复杂度为O(n^2),在大规模数据集合上性能较差,所以通常不推荐在高性能要求的场景中使用。

2.6. 快速排序:

  • 相对好情况:当待排序数组的划分相对平衡时,快速排序能够提供较好的性能,并且具有较好的空间局部性;
  • 最适用场景:适用于各种规模的数据集合,尤其在需要高效排序和适应动态数据变化的场景下表现优异。
  • 不适合使用的场景:在面对近乎有序的数据集合时,快速排序的性能可能退化为O(n^2),这是因为快速排序的划分可能导致不平衡的子问题。因此,在这种情况下,可以考虑其他排序算法。

2.7. 堆排序:

  • 相对好情况:堆排序在任何情况下都能保持O(nlogn)的时间复杂度,相对稳定而高效;
  • 最适用场景:适用于大规模数据集合的排序,特别是在要求稳定排序和不占用额外空间的场景下。
  • 不适合使用的场景:堆排序的主要缺点是需要额外的空间来存储堆,并且对于相同关键字的元素排序时无法保持它们原本的相对顺序。因此,在对空间要求严格或需要稳定排序的场景下,不适合使用堆排序。

2.8. 计数排序:

  • 相对好情况:当待排序数组的范围较小且分布均匀时,计数排序能够以线性时间复杂度进行排序;
  • 最适用场景:适用于非负整数等数据范围较小且分布均匀的情况,具有较高时间效率的场景下。
  • 不适合使用的场景:数排序的适用范围相对较窄,只适用于元素值分布范围不大的情况。如果待排序的元素值分布非常广泛,例如数值范围远远超过待排序元素个数,那么计数排序的空间复杂度将变得非常高,因此在这种情况下不适合使用计数排序。还有一点就是,计数排序只能用于对整型的排序!

总结

在实际应用中,选择排序算法需要综合考虑以下因素:

  1. 数据规模:不同排序算法的时间复杂度和空间复杂度对数据规模的影响不同,需要根据具体数据规模来选择排序算法。

  2. 数据分布:不同排序算法对数据分布的适应性也不同,例如计数排序适合数据分布较为均匀的情况,快速排序对数据分布的要求相对较低。

  3. 时间要求:每个排序算法的时间复杂度不同,对于时间要求严格的场景需要选择较为高效的算法,例如堆排序和快速排序。

  4. 稳定性:如果排序结果需要保持稳定,需要选择稳定排序算法,例如归并排序和计数排序。

  5. 额外空间消耗:有些排序算法需要额外的空间来保存中间结果,例如归并排序和堆排序,对于空间要求严格的场景需要避免使用这些算法。

在实际应用中,需要结合具体场景来选择最适合的排序算法。例如在需要对大规模数据进行排序且时间要求较高时,可以考虑采用快速排序或堆排序在数据分布较为均匀且需要稳定排序时,可以考虑采用计数排序或归并排序等稳定排序算法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Q_hd

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值