大部分基础排序复习C++(归并、快排、堆)

12 篇文章 0 订阅
6 篇文章 0 订阅

归并排序(稳定)

时间复杂度: O(N * logN ) 。空间复杂度 O(N)

归并排序主要是分治的思想:

将一段数组以中间点mid划分成两半,让它们各自去下一层递归,继续对半划分,直到这个数组只有一个元素时, 开始向上返回。每次返回都会将这这一层的两半数组(已经有序了)进行合并,合并成一个有序数组,继续向上返回,直到整个数组有序。

注意事项:在合并有序数组的时候,我们需要一个额外的容器,合并完之后再把结果填回到原数组中,对于这个额外的数组,建议使用全局的变量,这样能节省每层创建和销毁的开销,从而提高效率。 代码如下:

class Solution {
public:
    vector<int> tmp; // 全局的数组
    void mergeSort(vector<int>& nums,int left,int right)
    {
        if(left >= right) return;
        int mid = (right - left) / 2 + left;
        mergeSort(nums,left,mid);
        mergeSort(nums,mid + 1,right);

        // 开始合并两个数组
        int i = 0,l = left,r = mid + 1;
        while(l <= mid && r <= right)
            tmp[i++] = nums[l] <= nums[r] ? nums[l++] : nums[r++];
        while(l <= mid) tmp[i++] = nums[l++];
        while(r <= right) tmp[i++] = nums[r++];

        // 将结果填回到原数组中
        for(int i = left; i <= right; ++i)
            nums[i] = tmp[i - left];
    }

    vector<int> sortArray(vector<int>& nums) {
        int n = nums.size();
        tmp.resize(n); // 记得把空间开好
        mergeSort(nums,0,n - 1);
        return nums;
    }
};

快速排序(不稳定)

时间复杂度:O(N * logN),空间复杂度 : O(logN) 

也是分治的思想。用数组划分的形式来对数组进行排序。

大致流程:

首先选定一个元素基准值key,将数组划分为 小于等于key 和大于key的两个部分。然后每一层继续划分,直到只有一个元素为止。数组划分也是快排最重要的一部分。但是如果这个数组中有很多重复值时,最差情况下,基准值会到数组的最左或者最右端,这样就导致了时间复杂度退化到了O(N^2)。

刚刚的想法是将数组分为两块,我们可以用将数组分三块的方式进行优化:

 对于数组元素 == key的那一块,我们后续就不用再对它进行划分了。

核心流程:

定义三个变量, i :扫描数组,left :代表左区间已探知的下标,因为刚刚开始时一个都还没有探索,所以它相当于数组的 0 - 1 = -1的位置。 right:代表右区间已探索的下标,同理。

另外就是遍历的细节了:分为三种情况:

1. nums[i] < key,swap(nums[++left],nums[i++])。这里的i++很好理解,因为这里的元素已经探索过了。

2.nums[i] == key ,i++。

3.nums[i] > key。 swap(nums[++right],nums[i])。注意:这里的i是不要++的,因为我们遍历数组是从左往右遍历的,从右侧交换过来的数据是没有遍历过的,因此i不需要向前移动。

遍历完之后,我们只要再让 <key和>key的区间再进行划分即可。

这样如果数组中全是重复的元素时,反而可以把时间复杂度降至O(N)。

关于其他优化的地方:

对于基准值的选择:

可以用简单粗暴的 三数取中法,但是如果想让算法时间复杂度接近 O(N LogN),应该使用随机的方式取基准值(算法导论中提到过)。 

class Solution {
public:
    int GetRandom(vector<int>& nums,int left,int right)
    {
        int r = rand();
        r %= right - left + 1;
        r += left;
        return nums[r];
    }

    void qsort(vector<int>& nums,int l,int r)
    {
        if(l >= r) return;

        int key = GetRandom(nums,l,r); // 获取基准值

        // 数组分三块
        int left = l - 1,right = r + 1,i = l;
        while(i < right) // 注意这里是小于right,遍历过的区间不要再遍历了
        {
            if(nums[i] < key) swap(nums[++left],nums[i++]);
            else if(nums[i] == key) ++i;
            else swap(nums[--right],nums[i]);
        }
        qsort(nums,l,left);
        qsort(nums,right,r);
    }

    vector<int> sortArray(vector<int>& nums) {
        srand(time(NULL)); // 种随机数种子
        qsort(nums,0,nums.size() - 1);

        return nums;
    }
};

堆排序(不稳定)

先在原先数组的基础上构造大根堆(时间复杂度nlogn),向下调整,从倒数第一个父结点开始。
再依次弹出最大元素(每次弹出的时间复杂度为logk,k为当前大根堆中元素数目),弹出的元素就可以形成一个有序的数组。每弹出一次都要进行一次向下调整,把剩下的最大的元素放到堆顶的位置。

时间复杂度:O (N * logN),空间复杂度: O(1)

先简单复习一下堆的实现

#pragma once
#include<iostream>
#include<vector>
#include<functional>
namespace hzj
{
	using namespace std;
	template<class T, class Container = vector<T>, class Compare = less<T>>
	class priority_queue
	{
	private:
		void AdjustUp(int child)
		{
			int parent = (child - 1) / 2;
			while (child > 0)
			{
				if (_com(_c[parent],_c[child]))
				{
					std::swap(_c[parent], _c[child]);
					child = parent;
					parent = (child - 1) / 2;
				}
				else break;
			}
		}
		void AdjustDown(int parent)
		{
			int child = parent * 2 + 1;
			int len = size();
			while (child < len)
			{
				if (child + 1 < len && _com(_c[child] , _c[child + 1])) child++;
				if (_com(_c[parent],_c[child]))
				{
					std::swap(_c[child], _c[parent]);
					parent = child;
					child = child * 2 + 1;
				}
				else break;
			}
		}
	public:
		priority_queue()
		{}
		template<class inputiterator>
		priority_queue(inputiterator first, inputiterator last)
		{
			inputiterator it = first;
			while (it != last)
			{
				_c.push_back(*it);
				++it;
			}
			for (int i = (size() - 2) / 2; i >= 0;i--)
			{
				AdjustDown(i);
			}
		}
		bool empty()const
		{
			return _c.empty();
		}
		size_t size()const
		{
			return _c.size();
		}
		const T& top()
		{
			return _c[0];
		}
		void push(const T& x = new T())
		{
			 _c.push_back(x);
			 AdjustUp(size() - 1);
		}
		void pop()
		{
			std::swap(_c[0], _c[size() - 1]);
			_c.pop_back();
			AdjustDown(0);
		}
	private:
		Container _c; // 容器
		Compare _com; // 比较方法
	};
}

代码:

class Solution {
public:
    void buildMaxHeap(vector<int>& nums)
    {
        int n = nums.size();
        // 从倒数第一个有子结点的父结点开始调整
        for(int i = (n - 1) / 2; i >= 0; --i)
        {
            AdjustDown(nums,i,n);
        }
    }

    // 向下调整,i表示调整的起点,n表示需要调整区域的大小,在排序时是不断变小的,建堆时不变
    void AdjustDown(vector<int>& nums,int i,int n)
    {
        while(i * 2 + 1 < n)
        {
            int next = i; // 方便后续用于更新i的值
            int lson = i * 2 + 1; // 左孩子
            int rson = i * 2 + 2; // 右孩子
            // 找到最大的那个孩子的下标,这里决定大根堆还是小根堆
            if(lson < n && nums[lson] > nums[i]) next = lson;
            if(rson < n && nums[rson] > nums[next]) next = rson;

            if(i != next) // 找到了就更新i的值,还有记得交换结点的值
            {
                swap(nums[i],nums[next]);
                i = next;
            }
            else // 反之说明不用继续调整了
            {
                break;
            }
        }
    }

    vector<int> sortArray(vector<int>& nums) {
        int n = nums.size();
        buildMaxHeap(nums); // 建堆(此处为大根堆)
        // 依次弹出最大的元素,把整个数组全部弹出
        for(int i = n - 1; i >= 0; --i)
        {
            swap(nums[0],nums[--n]); // 弹出的元素可以直接放在数组末尾
            AdjustDown(nums,0,n); // 向下调整,把剩下最大的元素放到堆顶
        }
        return nums;
    }
};

插入排序(稳定)

在前 0 ~ i - 1 元素有序的情况下,(依次)将第 i 个元素插入前面已经有序的小序列,先使用二分查找找到插入位置的下标index,然后将区间[index + 1,i] 往后移动,再把原来i位置的值插入到index位置,使其有序。 

时间复杂度:O(N ^ 2 ),空间复杂度 O(1)。

class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        int n = nums.size();
        for(int i = 1; i < n; ++i)
        {
            // 如果已经有序了那么就直接插入
            if(nums[i] >= nums[i - 1]) continue;
            
            int l = 0,r = i - 1;
            // 二分查找 时间复杂度 O(logN)
            while(l < r)
            {
                int mid = (r - l) / 2 + l;
                if(nums[i] > nums[mid]) l = mid + 1;
                else r = mid;
            }
            int index = l;
            int tmp = nums[i];
            // 移动数组 时间复杂度 O(N)
            for(int k = i; k >= index + 1; --k)
            {
                nums[k] = nums[k - 1];
            }
            nums[index] = tmp;
        }

        return nums;
    }
};

 希尔排序(不稳定)

改进的插入排序(优化:原数组的一个元素距离正确位置很远的情况)
先让间隔 h 的元素有序,在使得间隔为 h / 2,一直缩小,一直到 h = 1(此时数组有序)。

也就是先将数组划分为 n / h 个组,预排序的作用就是为了让每个组之间尽可能的有序。 

当间隔等于1时就相当于再进行了一次插入排序,而插入排序是数据在有序的情况下时间复杂度为O(N),在逆序的情况下最差为O(N ^ 2 ),经过预处理后,数组已经尽可能的接近有序了 。

也就是在插入排序前先进行预排序。

时间复杂度:O(N * logN) 空间复杂度: O(1)。

class Solution {
public:
    void shellSort(vector<int>& nums,int gap,int i)
    {
        int j = i - gap,tmp = nums[i];
        for(; j >= 0; j -= gap)
        {
            if(tmp < nums[j]) // 向后移动gap步
            {
                nums[j + gap] = nums[j];
            }
            else // 到这里就停止了,这里就是tmp该插入的位置
            {
                break;
            }
        }
        nums[j + gap] = tmp;
    }

    vector<int> sortArray(vector<int>& nums) {
        int n = nums.size();
        for(int gap = n / 2; gap >= 1; gap /= 2)
        {
            // 对各个分组进行插入分组
            for(int i = gap; i < n; ++i)
            {
                shellSort(nums,gap,i);
            }
        }

        return nums;
    }
};

另一种写法:

class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        int n = nums.size();
        int gap = n;
        while(gap > 1)
        {
            gap = gap / 3 + 1;
            // gap /= 2; 也可以
            for(int i = 0; i < n - gap; ++i)
            {
                int tmp = nums[i + gap];
                int end = i;
                while(end >= 0)
                {
                    if(nums[end] > tmp) // 向后移动
                    {
                        nums[end + gap] = nums[end];
                        end -= gap;
                    }
                    else 
                    {
                        break;
                    }
                }
                nums[end + gap] = tmp;
            }
        }

        return nums;
    }
};

冒泡排序(稳定)

比较相邻的元素,如果第一个比第二个大,那么就交换

时间复杂度:O(N ^ 2 ),空间复杂度:O(1)

代码:

class Solution {
public:
    vector<int> sortArray(vector<int>& nums) 
    {
        // bubbleSort 冒泡
        int n = nums.size();
        for (int i = 0; i < n - 1; ++i) 
        {
            bool flag = false;
            for (int j = 0; j < n - 1 - i; ++j) 
            {
                if (nums[j] > nums[j + 1]) 
                {
                    swap(nums[j], nums[j + 1]);
                    flag = true;
                }                 
            }
            if (flag == false) break; //无交换,代表当前序列已经最优 
        }
        return nums;
    }
};

选择排序(不稳定)

用i下标从左往右遍历数组,将区间  [i,n - 1]的最小值与i位置的值进行交换。

时间复杂度:O(N ^ 2),空间复杂度:O(1)

代码:

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值