【C++】十大排序算法深究(冒泡、插入、选择、快速、归并、堆、计数、桶)


排序算法基础知识

1、时间复杂度O(n^2)的排序算法:冒泡、选择、插入、希尔
2、时间复杂度O(nlogn)的排序算法:快速、归并、堆
3、时间复杂度O(n)的排序算法:计数、桶等

排序算法稳定性分析:如果值相同的元素在排序后仍保持着排序前的顺序,则称为稳定排序;否则为不稳定排序。

在这里插入图片描述


一、Bubble Sort(冒泡排序)

冒泡排序 升序 时间复杂度O(n^2)

// 冒泡排序 升序 时间复杂度O(n^2)
void bubbleSort(vector<int>& vec)
{
    for (int i = 0; i < vec.size()-1; ++i)
    {
        for (int j = 0; j < vec.size() - i - 1; ++j)
        {
            if (vec[j] > vec[j + 1])
            {
                int temp = vec[j];
                vec[j] = vec[j + 1];
                vec[j + 1] = temp;
            }
        }
    }
}

基于冒泡排序的升级排序法,鸡尾酒排序法。


二、选择排序

选择排序是给每个位置选择当前元素最小的,比如第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,以此类推,直到第n-1个元素,第n个元素就不用进行处理了(已经是剩余元素中最小的了)


时间复杂度O(n^2) 空间复杂度O(1) 非稳定排序 原地排序

/*
    选择排序是给每个位置选择当前元素最小的,比如第一个位置选择最小的,在剩余元素里面给第二个元素选择
    第二小的,以此类推,直到第n-1个元素,第n个元素就不用进行处理了(已经是剩余元素中最小的了)

    时间复杂度O(n^2) 空间复杂度O(1) 非稳定排序 原地排序
*/
void selectionSort(vector<int>& nums)
{
    // 记录未排序序列中最小(大)元素下标
    int minIndex = 0;
    for(int i = 0; i < nums.size(); ++i)
    {
        // 从剩余未排序序列中寻找最小(大)元素,然后放到已排序序列的末尾,以此类推,直到所有元素均排序完毕
        minIndex = i;
        for(int j = i + 1; j < nums.size(); ++j)
        {
            if(nums[j] < nums[minIndex])
            {
                minIndex = j;
            }
        }
        // 将当前序列中的最小元素交换到已排序序列的末尾
        swap(nums[i], nums[minIndex]);
    }
}

三、插入排序

介绍:插入排序是在一个已经有序的小序列的基础上,一次插入一个元素,刚开始这个有序的小序列只有1个元素,就是第一个元素。比较是从有序序列的末尾开始,也就是想要插入的元素和 已经有序的最大者 开始比起,如果比 已经有序的最大者 大 则直接插入在其后面,否则一直往前找直到找到它该插入的位置。


如果碰见一个 和插入元素相等 的,那么插入元素把想插入的元素放在相等元素的后面(插入排序是稳定的)。
时间复杂度O(n^2) 空间复杂度O(1)

void insertSort(vector<int>& nums)
{
    // 从第一个元素开始遍历,认为第0个元素是已经有序的一个小序列
    for(int i = 1; i < nums.size(); ++i)
    {
        // 当待排序元素 小于 有序序列末尾的元素
        if(nums[i] < nums[i - 1])
        {
            // 将有序序列向后移动,将当前元素插入到合适的位置

            // 记录有序序列末尾的元素下标
            int j = i - 1;
            // 存储待排序元素
            int temp = nums[i];

            // 移动有序序列(有序序列后移,腾出待插入元素的位置)
            while(j >= 0 && nums[j] > temp)
            {
                // 元素后移
                nums[j + 1] = nums[j];
                --j;
            }
            // 将待插入的元素插入到合适的位置
            nums[j + 1] = temp;
        }
    }
}

四、Quick Sort(快排)

分治法

// 双边循环法
int partionV1(vector<int>& vec, int startIndex, int endIndex)
{
    // 取第一个元素作为基准元素,也可以取随机位置元素
    int pivot = vec[startIndex];
    int left = startIndex;
    int right = endIndex;
    while (left != right)
    {
        // 如果right指针指向的元素大于或等于pivot,指针左移;否则,指针停止移动,切换到left指针
        while (left<right && vec[right] >= pivot)
            --right;
        // 如果left指针指向的元素小于或等于pivot,指针右移;否则,指针停止移动
        while (left < right && vec[left] <= pivot)
            ++left;
        
        // 交换对应不符合条件的元素
        if (left < right)
        {
            int temp = vec[left];
            vec[left] = vec[right];
            vec[right] = temp;
        }
    }
    // left==right 跳出while循环
    // 将指针重合点元素与基准元素交换,该轮排序结束
    vec[startIndex] = vec[left];
    vec[left] = pivot;

    return left;// 返回最终基准元素的下标
}
// 单边循环法
int partionV2(vector<int>& vec, int startIndex, int endIndex)
{
	 确定基准元素 选择区间内的随机下标(避免初始序列有序的情况出现,导致时间复杂度降为O(n^2))
    //int index = beginIndex + rand() % (endIndex - beginIndex + 1);
     将随机选择的元素与区间内首个元素进行交换
    //swap(nums[beginIndex], nums[index]);
    
    // 取第一个元素作为基准元素
    int pivot = vec[startIndex];
    int mark = startIndex;
    for (int i = startIndex + 1; i <= endIndex; ++i)
    {
        // 基准元素左边<=pivot 右边>pivot
        if (vec[i] < pivot)
        {
            ++mark;
            int temp = vec[i];
            vec[i] = vec[mark];
            vec[mark] = temp;
        }
    }
    // 将基准元素与mark位置元素进行交换
    vec[startIndex] = vec[mark];
    vec[mark] = pivot;
    return mark;
}
// 快速排序 分治法 左闭右闭区间
void quickSort(vector<int>& vec, int startIndex, int endIndex)
{
    // 递归终止条件
    if (startIndex >= endIndex)
        return;
    // 获取基准元素位置 这里可以选择使用双边循环法和单边循环法
    // int pivotIndex = partionV1(vec, startIndex, endIndex);
    int pivotIndex = partionV2(vec, startIndex, endIndex);

    // 确定单层递归的逻辑
    quickSort(vec, startIndex, pivotIndex - 1);
    quickSort(vec, pivotIndex+1, endIndex);
}

五、计数排序

/*
    计数排序是一种线性时间的整数排序算法。如果数组长度为n,整数范围(数组中最大整数与最小整数之间的差值)
    为k,对于k远小于n的场景,那么计数排序的时间复杂度优于归并、快排等。

    基本思想:先统计数组中每个元素在数组中出现的次数,然后按照从小到大的顺序
            将每个元素按照它出现的次数填到数组中
*/
// 计数排序
void numSort(vector<int>& vec)
{
    // 记录数组中最大、最小整数
    int minValue = INT_MAX;
    int maxValue = INT_MIN;
    for(int num : vec)
    {
        minValue = min(minValue, num);
        maxValue = max(maxValue, num);
    }

    // 创建大小为k+1的数组,因为后续要访问到k,所以需要+1
    // 该数组统计元素在原数组中出现的次数
    int k = maxValue - minValue;
    vector<int> counts(k + 1, 0);
    for(int num : vec)
    {
        ++counts[num - minValue];
    }

    // 按从小到大的顺序将每个整数按其出现的次数填到数组中
    int i = 0;// 索引
    // 这里为排序的顺序,也可以按照某个数组的相对顺序进行排序
    for(int num = minValue; num <= maxValue; num++)
    {
        while(counts[num - minValue] > 0)
        {
            vec[i++] = num;
            --counts[num - minValue];
        }
    }
    // 例如:arr1 = {3,2,1} 按照arr1的相对顺序对原数组进行排序
    // for(int num : arr1)
    // {
    //     while(counts[num] > 0)
    //     {
    //         vec[i++] = num;
    //         --counts[num];
    //     }
    // }
}

如果数组长度为n,数组整数范围为k,那么计数排序的时间复杂度为O(n + k),需要一个大小为k的辅助数组,故空间复杂度为O(k)


六、桶排序

桶排序思想:

每一个桶(bucket)代表一个区间范围,里面可以承载一个或多个元素

桶是从大到小排列的,并且每一个桶都会有一个数据范围。当把元素放入对应的桶里面的时候,需要对桶内的序列进行排序(保持桶内序列有序)。最后按照桶的序号将桶内的元素打印出来即可得到有序序列。

  1. 桶的数量过多或者过少都会影响桶排序的效率
  2. 元素分布极不均匀,在极端的情况下,第一个桶中有n-1个元素,在最后一个桶中有1个元素,时间复杂度将退化为O(nlogn),且浪费了很多空间

算法实现:

  • 遍历序列,找到序列中的最大值和最小值(确定数据范围[minVal, maxVal])
  • 创建桶,并确定每一个桶的区间范围
    • 区间跨度 = (最大值 - 最小值)/(桶的数量 - 1)
  • 遍历原始数列,把元素对号入座放入各个桶中
  • 对每个桶内部的元素分别进行排序
  • 遍历所有的桶,输出所有元素
// 链表节点
struct ListNode{
    int val;
    ListNode* next;
	
	ListNode() : val(0), next(nullptr) {}
    ListNode(int val) : val(val), next(nullptr) {}
    ListNode(int val, ListNode* next) : val(val), next(next) {}
};

// 向有序链表中插入节点
ListNode* insert(ListNode* head, int val)
{
    // 创建一个虚拟头节点
	ListNode* dummyHead = new ListNode(-1);
    // 虚拟头节点指向链表头节点
    dummyHead->next = head;

    // 创建待插入的节点
	ListNode* newNode = new ListNode(val);

	// 创建双指针
    ListNode* pre = dummyHead;
    ListNode* cur = dummyHead->next;
	

    // 移动双指针到合适的位置
	while(cur != nullptr && val >= cur->val)
    {
		pre = cur;
		cur = cur->next;
	}
    // 插入节点
	newNode->next = cur;
	pre->next = newNode;

    head = dummyHead->next;
    // delete dummyHead;

	return head;
}

class comp
{
public:
    bool operator()(ListNode* a, ListNode* b)
    {
        return a->val > b->val;
    }
};
// 合并k个有序链表
ListNode* mergeKLists(vector<ListNode*>& lists) 
{
    // 使用优先队列对合并链表算法时间复杂度进行优化
    // 注意:优先队列右边为队首,即return a->val > b->val; 这种方式构造出来的堆为小顶堆,小的元素优先出队
    priority_queue<ListNode*, vector<ListNode*>, comp> pri;

    // 向队列中添加元素
    for(ListNode* l : lists)
    {
        if(l != nullptr)
            pri.push(l);
    }

    // 创建虚拟头节点
    ListNode* dummyHead = new ListNode(-1);
    // 创建指针,指向新链表
    ListNode* cur = dummyHead;

    // 队列非空,不断对队首链表进行挂接、出队操作
    while(!pri.empty())
    {
        // 记录优先队列队首链表元素
        ListNode* node = pri.top();
        pri.pop();

        // 新链表挂接队首链表的头节点
        cur->next = node;
        // 更新新链表上的指针
        cur = cur->next;

        // 如果队首链表除头节点外还不为空,需要将剩余的链表重新添加到优先队列中
        if(node->next != nullptr)
            pri.push(node->next);
    }
    return dummyHead->next;
}

// 桶排序
// bucketCount为桶排序中桶的数量
void bucketSort(vector<int>& vec, int bucketCount)
{
    if(vec.size() == 0) return;

    // 每个桶的区间比例:
    // 区间跨度 = (最大值 - 最小值)/(桶的数量 - 1)

    int minVal = INT_MAX;
    int maxVal = INT_MIN;

    for(int num : vec)
    {
        minVal = min(minVal, num);
        maxVal = max(maxVal, num);
    }

    // double size = (maxVal - minVal + bucketCount) / bucketCount;
    double d = maxVal - minVal;

    // 初始化桶
    // 创建bucketCount个桶,每个桶都是一个链表
    vector<ListNode*> buckets(bucketCount, nullptr);

    // 遍历数组,将每个元素放入对应的桶中
    for(int i = 0; i < vec.size(); ++i)
    {
        // 第一种求当前元素对应桶的索引
        // double size = (maxVal - minVal + bucketCount) / bucketCount;
        // int index = (int) ((vec[i] - minVal) / size);

        // 第二种求当前元素对应桶的索引(书上)
        // double d = maxVal - minVal;
        int index = (int) ((vec[i] - minVal) * (bucketCount - 1) / d);
        // 找到对应的桶,向对应的桶内插入元素
        // ListNode* head = buckets[index];
        buckets[index] = insert(buckets[index], vec[i]);
    }

    // 合并k个链表
    ListNode* head = mergeKLists(buckets);
    
    // 打印输出排序后的元素或者直接改变原数组vec中的值
    for(int i = 0; i < vec.size(); ++i)
    {
        vec[i] = head->val;
        head = head->next;
    }

}

int main()
{
    vector<int> vec{7, 4, 9, 3, 2, 1, 8, 6, 5, 10};
    bucketSort(vec, 10);

    for(int n : vec)
    {
        std::cout << n << " ";// 1 2 3 4 5 6 7 8 9 10 
    }
    
    return 0;
}

每个桶都是一个链表,便于元素的增删操作。
假设原始序列有n个元素,分成n个桶:桶排序的时间复杂度O(n) 空间复杂度O(n)
注意:在每个桶内部做排序,在元素分布相对均匀的情况下,所有桶的运算量之和为n


七、Merge Sort(归并排序)

// 归并排序:为了排序长度为n的数组,需要先排序两个长度为n/2的子数组,然后合并这两个排序的子数组,整个数组排序完毕
// 左闭右闭区间
// src存放合并之前的数组,tempArr存放合并之后的数组
void merge(vector<int>& src, int startIndex, int mid, int endIndex);
void mergeSort(vector<int>& src, int startIndex, int endIndex)
{
    // 递归结束条件
    if(startIndex >= endIndex)
        return;
    
    int mid = startIndex + (endIndex - startIndex) / 2;// 取区间中间元素下标
    mergeSort(src, startIndex, mid);
    mergeSort(src, mid + 1, endIndex);

    // 将两个有序的数组合并成一个大数组
    merge(src, startIndex, mid, endIndex);
}
// 将两个有序数组合并成一个大数组
void merge(vector<int>& src, int startIndex, int mid, int endIndex)
{
    // 创建新数组,用来存储合并后的数组
    // 注意:新数组长度为endIndex - startIndex + 1,是每个子区间的长度,不应该是src原数组的长度,注意不要乱声明大小
    vector<int> tempArr(endIndex - startIndex + 1, 0);

    // 进来的两个有序数组区间[startIndex, mid] [mid + 1, endIndex]
    int p1 = startIndex;
    int p2 = mid + 1;
    int p = 0;// 新数组中的索引

    // 比较两个有序小数组的元素,并依次放入大数组中
    while(p1 <= mid && p2 <= endIndex)
    {
        if(src[p1] <= src[p2])
        {
            tempArr[p++] = src[p1++];
        }
        else
        {
            tempArr[p++] = src[p2++];
        }
    }

    // 循环结束,判断小数组是否还有剩余元素,这里只可能有一个小数组有剩余元素,即sec长度为奇数的情况
    while(p1 <= mid)
        tempArr[p++] = src[p1++];
    while(p2 <= endIndex)
        tempArr[p++] = src[p2++];
    
    // 将合并之后的新数组元素赋值到原数组src中
    for(int i = 0; i < tempArr.size(); i++)
    {
        src[startIndex + i] = tempArr[i];
    }
}

八、Heap Sort(堆排序)

堆:完全二叉树 + 所有父节点的值大于子节点的值
将堆用数组表示,第i个节点的 父节点位置 = (i - 1) / 2 孩子节点 c1 = 2i + 1, c2 = 2i + 2

// 确定递归函数参数和返回值 n为堆节点个数,i表示从第i个节点开始进行heapify
// 对有一定顺序的堆,当前第i个节点位置替换为根左右的最大值
void heapify(vector<int>& nums, int n, int i)
{
    // 确定递归终止条件
    if(i >= n) return;// 索引越界

    // 计算当前i节点的左右孩子节点位置
    int c1 = 2 * i + 1;
    int c2 = 2 * i + 2;

    // 记录第i个节点和孩子节点中的最大节点位置
    int max = i;
    if(c1 < n && nums[c1] > nums[max]) max = c1;
    if(c2 < n && nums[c2] > nums[max]) max = c2;

    // 将第i个节点和左右孩子节点三个节点中的最大值,交换到i的位置,即作为当前三个节点的堆顶
    if(max != i)
    {
        swap(nums[i], nums[max]);

        // 交换后,需要对交换后的位置的节点进行heapify,不断递归,直到全部满足堆的条件
        heapify(nums, n, max);
    }
    // 相等,则说明当前节点就是三个节点的堆顶节点
}

// 创建堆
// nums为堆数组,n为数组大小
void build_heap(vector<int>& nums, int n)
{
    int last_node = n - 1;
    // 找到最后一个节点的父节点
    int parent = (last_node - 1) / 2;

    // 从最后一个节点的父节点开始,不断进行heapify
    // 3 - 2 - 1 - 0
    for(int i = parent; i >= 0; --i)
    {
        heapify(nums, n, i);
    }
}

// 进行堆排序
void heapSort(vector<int>& nums, int n)
{
    // 首先根据数组创建堆
    build_heap(nums, n);

    // 交换堆顶节点与堆最后一个节点,将堆顶最大节点交换到最后一个节点位置
    // 然后截掉最后一个节点,就是弹出最大值,接着需要对堆重新进行heapify(每次都需要从根节点开始,因为将小节点交换到了根节点位置)
    for(int i = n - 1; i >= 0; --i)
    {
        swap(nums[0], nums[i]);
		// 注意此时需要进行heapify的堆的节点个数为i (相当于不考虑弹出的最大值节点)
        heapify(nums, i, 0);
    }
}

int main()
{
    vector<int> nums{3, 44, 5, 27, 2};
    heapSort(nums, nums.size());
	
	return 0;
}

九、希尔排序

希尔排序可以说是插入排序的一种变种。无论是插入排序还是冒泡排序,如果数组的最大值刚好是在第
一位,要将它挪到正确的位置就需要 n - 1 次移动。也就是说,原数组的一个元素如果距离它正确的位
置很远的话,则需要与相邻元素交换很多次才能到达正确的位置,这样是相对比较花时间了。

希尔排序就是为了加快速度简单地改进了插入排序,交换不相邻的元素以对数组的局部进行排序。
希尔排序的思想是采用插入排序的方法,先让数组中任意间隔为 h 的元素有序,刚开始 h 的大小可以是
h = n / 2,接着让 h = n / 4,让 h 一直缩小,当 h = 1 时,也就是此时数组中任意间隔为1的元素有序,此时
的数组就是有序的了。

希尔排序 非稳定排序 时间复杂度O((nlogn)^2) 空间复杂度O(1)
void shellSortCore(vector<int>& nums, int gap, int i) {
    int inserted = nums[i];
    int j;
    // 插入的时候按组进行插入
    for (j = i - gap; j >= 0 && inserted < nums[j]; j -= gap) 
    {
        nums[j + gap] = nums[j];
    }
    nums[j + gap] = inserted;
}

void shellSort(vector<int>& nums) {
    int len = nums.size();
    //进行分组,最开始的时候,gap为数组长度一半
    for (int gap = len / 2; gap > 0; gap /= 2) 
    {
        //对各个分组进行插入分组
        for (int i = gap; i < len; ++i) 
        {
            //将nums[i]插入到所在分组正确的位置上
            shellSortCore(nums,gap,i);
        }
    }
}

十、基数排序

对数组中所有数依次按由低到高的位数进行多次排序; 每次排序都基于上次排序的结果。 (相对位置顺序保持不变)

class Solution 
{
private:
    void radixSort(vector<int>& nums, vector<int>& tmp, int divisor) 
    {
        int n = nums.size();
        vector<int> counts(10, 0);
        // 统计对应个十百千万上对应数字出现的次数
        for (int i = 0; i < n; ++i) 
        {
            int x = (nums[i] / divisor) % 10;
            if (x != 9) ++counts[x + 1];
        }
        // 前缀和
        for (int i = 1; i <= 9; ++i) 
        {
            counts[i] += counts[i - 1];
        }
        // 从前向后赋值
        for (int i = 0; i < n; ++i) 
        {
            int x = (nums[i] / divisor) % 10;
            tmp[counts[x]++] = nums[i];  
        }
    }

public:
    vector<int> sortArray(vector<int>& nums) {
        // RadixSort 基数排序
        int n = nums.size();
        // 预处理,让所有的数都大于等于0 -5 * 10^4 <= nums[i] <= 5 * 10^4
        for (int i = 0; i < n; ++i) 
        {
            nums[i] += 50000; // 50000为最小可能的数组大小
        }
        // 找出最大的数字,并获得其最大位数
        int maxNum = nums[0];
        for (int i = 0; i < n; ++i) 
        {
            if (nums[i] > maxNum) 
            {
                maxNum = nums[i];
            }
        }
        int num = maxNum, maxLen = 0;
        while (num) 
        {
            ++maxLen;
            num /= 10;
        }
        // 基数排序,低位优先
        int divisor = 1;
        vector<int> tmp(n, 0);
        for (int i = 0; i < maxLen; ++i) 
        {
            radixSort(nums, tmp, divisor);
            swap(tmp, nums);
            divisor *= 10;
        }
        // 减去预处理量
        for (int i = 0; i < n; ++i) {
            nums[i] -= 50000;
        }
        return nums;
    }
};

总结

提示:这里对文章进行总结:

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值