考研算法(七) 排序

1.直接插入排序

给定一个整数数组 nums,将该数组升序排列。

思想

往前寻找插入位置,如果找到了大于等于它的元素或者没有找到,数组越界,查找停止,将元素插入对应位置。否则继续,将当前位置元素后移。即以第一个元素作为有序数组,其后的元素通过在这个已有序的数组中找到合适的位置并插入。

空间:O(1)

最好:O(n),初始为有序时

最坏:O(n^2) ,初始为是逆序时

平均:O(n^2)

稳定性:是

    vector<int> sortArray(vector<int>& nums) {
        int n = nums.size();
        for(int i = 1,j; i < n; i++){//此时0~i-1已有序
            int v = nums[i];//i位置元素待插入
            for(j = i-1; j>=0&&v<nums[j]; j--){//向前寻找插入位置
                nums[j+1]=nums[j];
            }
            nums[j+1] = v;//插入
        }
        return nums;
    }

2.折半插入排序

给定一个整数数组 nums,将该数组升序排列。

思想

  1. 与直接插入排序的唯一区别就是查找插入位置时 使用二分,复杂度不变。
  2. 二分查找应该是logn才对,但是复杂度不变,因为后移元素需要O(n)的复杂度,所以虽然时间快了一点,但是复杂度是不变的。
    vector<int> sortArray(vector<int>& nums) {
        int n = nums.size();
        for(int i = 1; i < n; i++){//此时0~i-1已有序
            int v = nums[i];//i位置元素待插入

            int l = 0, r = i-1;//二分查找插入位置
            while(l<r){
                int mid = l+r+1 >> 1;
                if(nums[mid]<=v) l = mid;
                else r = mid-1;
            }
            if(nums[l]>v) l--;//防止在单个元素中二分的情况
            //此时的l即为插入位置的前一个位置

            for(int j = i-1; j > l; j--) nums[j+1] = nums[j];//后移
            nums[l+1] = v;//插入
        }
        return nums;
    }

3.希尔排序

给定一个整数数组 nums,将该数组升序排列。

思想

类似插入排序,只是向前移动的步数变成d,插入排序每次都只是向前移动1。

空间:O(1)

平均:当n在特定范围时为 O(n^1.3)

稳定性:否

    vector<int> sortArray(vector<int>& nums) {
        int n = nums.size();
        for (int d = n / 2; d >= 1; d /= 2) {//d为增量 d=1时就是0x00的插入排序
            for (int i = d,j; i < n; i++) {
                int v = nums[i];//i位置元素待插入
                for (j = i-d; j >= 0 && v < nums[j]; j -= d) {//以增量d向前寻找插入位置
                    nums[j+d] = nums[j];
                }
                nums[j+d] = v;
            }
        }
        return nums;
    }

4.冒泡排序

给你一个整数数组 nums,请你将该数组升序排列。

思想

通过相邻元素的比较和交换,使得每一趟循环都能找到未有序数组的最大值

空间:O(1)

最好:O(n),初始即有序时 一趟冒泡即可

最坏:O(n^2), 初始为逆序时

平均:O(n^2)

稳定性:是

    vector<int> sortArray(vector<int>& nums) {
        int n = nums.size();
        bool sorted = false;//无序
        for(int i = n-1; i >=0 && !sorted; i--){//0~i为无序区 
            sorted = true;//假定已有序
            for(int j = 0; j < i; j++){//相邻比较 大的沉底
                if(nums[j]>nums[j+1]) swap(nums[j],nums[j+1]),sorted = false;//发生交换 则无序
            }
        }
        return nums;
    }

4.双向冒泡排序

给你一个整数数组 nums,请你将该数组升序排列。

思想

如同名字一样,先从左向右冒泡,把最大的元素右移到当前未有序区间的右边界,然后将有序区间的右边界左移。然后从右向左冒泡,把最小的元素左移到当前未有序区间的左边界,然后将有序区间的左边界右移。一直循环下去,直到左右边界相同,即区间中只有一个元素,已经有序,循环结束。或者标志为0,表明没有数据交换,未有序区间中所有元素有序,循环终止。

空间:O(1)

最好:O(n),初始即有序时 一趟冒泡即可

最坏:O(n^2)

平均:O(n^2)

稳定性:是

    vector<int> sortArray(vector<int>& a) {
            int n=a.size();
            int l = 0, r = n-1, flag;
            while(l < r)
            {
                flag=0;
                for(int i=l; i<r; i++)  //正向冒泡
                {
                    if(a[i] > a[i+1]) //找到剩下中最大的
                    {
                        swap(a[i], a[i+1]);
                        flag = 1;    //标志, 有数据交换
                    }
                }
                if( !flag )
                    break;
                r--;
				
				flag=0;
                for(int i=r; i>l; i-- ) //反向冒泡
                {
                    if(a[i] < a[i-1]){   //找到剩下中最小的
                        swap(a[i], a[i-1]);
                        flag = 1;    //标志, 有数据交换
                    }
                }
                l++;
                if( !flag )
                    break;
            }
            return a;
    }

6.快速排序

给你一个整数数组 nums,请你将该数组升序排列。

思想

叫快排不太合适,应该叫分递排序,这样体现它的算法流程特点。

选择一个元素作为基数(考研通常是第一个元素),把比基数小的元素放到它左边,比基数大的元素放到它右边(相当于二分),再不断递归基数左右两边的序列。

空间:O(log n),即递归栈深度

最好:O(nlog n) ,其他的数均匀分布在基数的两边,此时的递归就是不断地二分左右序列。

最坏:O(n^2) ,其他的数都分布在基数的一边,此时要划分n次了,每次O(n)

平均:O(nlog n)

稳定性:否

    void quick_sort(vector<int>& nums, int l , int r){//对下标为 l~r部分 排序
        if(l >= r ) return ;
        int x = nums[l], i = l-1, j = r+1;//x为划分中枢 i为左半起点 j为右半起点
        while(i < j){
            while(nums[++i] < x);//左边寻大于x的数
            while(nums[--j] > x);//右边寻小于x的数
            if(i < j) swap(nums[i],nums[j]);//每次把左边大于x的数和右边小于x的交换即可
        }
        quick_sort(nums,l,j),quick_sort(nums,j+1,r);
        //因为结束时i在j的右边 j是左半段的终点,i是右半段的起点
    }
    vector<int> sortArray(vector<int>& nums) {
        int n = nums.size();
        quick_sort(nums,0,n-1);
        return nums;
    }

7.快排应用-第k大数

在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

示例 1:

输入: [3,2,1,5,6,4] 和 k = 2

输出: 5

思想

  1. 我们希望这是一个有序的数组,直接返回下标为k-1的元素就行了。但是数组是无序的,所以可以将其变成一个有序数组,这样就解决了。但是快排需要nlogn的复杂度,可以优化一下,在排序的过程中找第k大数。快排每次将区间分成两份,左边的元素>=右边的元素,我们将左边区间的元素个数与k比较,k<=左半区间长度,则第k大数必然在左半段 并且是左半段的第k大数,否则第k大数必然在右半段 并且是右半段的第k-sl大数。
  2. 这其实就是递归版本的二分,区别在于二分是接近等分的,而快排划分的左右区间通常是不等分的,甚至一侧区间大小为n-1,一侧大小为1。
    int quick_select(vector<int>& nums, int l , int r, int k){
                                  //在下标为l~r的数中求第k大的数
        if(l >= r) return nums[l];
        int x = nums[l], i = l-1, j = r+1;
        while(i < j){//以x为枢纽 一次快排划分 此处大的在左边 小的在右边
            while(nums[++i] > x);
            while(nums[--j] < x);
            if(i < j) swap(nums[i],nums[j]);
        }
        int sl = j-l+1;//左半区间的长度
        if(sl>=k) return quick_select(nums, l,j,k);
        //k<=左半区间长度,则第k大数必然在左半段 并且是左半段的第k大数
        else return quick_select(nums, j+1, r, k - sl);
        //第k大数必然在右半段 并且是右半段的第k-sl大数
    }
    int findKthLargest(vector<int>& nums, int k) {
        int n = nums.size();
        return quick_select(nums,0,n-1,k);
    }

8.选择排序

给你一个整数数组 nums,请你将该数组升序排列。

思想

和冒泡排序相似,区别在于选择排序是直接挑出未排序元素的最大值放后面,其它元素不动。无论如何都要O(n^2)

最好:O(n^2)

最坏:O(n^2)

平均:O(n^2)

稳定性:否

   vector<int> sortArray(vector<int>& nums) {
        int n = nums.size();
        for(int i = n-1; i >=0; i--){//0~i为无序部分
            int minpos = -1;
            for(int j = 0; j <= i; j++){//寻找0~i区间内的最大值的位置
                if(minpos ==-1||nums[j]>nums[minpos]) minpos = j;
            }
            swap(nums[i],nums[minpos]);//把挑出最大值放到后面
        }
        return nums;
    }

9.堆排序

输入一个长度为n的整数数列,从小到大输出前m小的数。

输入格式
第一行包含整数n和m。

第二行包含n个整数,表示整数数列。

输出格式
共一行,包含m个整数,表示整数数列中前m小的数。

输入样例:

5 3

4 5 1 3 2

输出样例:

1 2 3

思想

根据数组建立一个堆(类似完全二叉树),每个结点的值都大于左右结点(大根堆,通常用于升序),或小于左右结点(小根堆堆,通常用于降序)。

空间:O(1)

最好:O(nlog n),log n是调整小根堆所花的时间

最坏:O(nlog n)

平均:O(nlog n)

稳定性:否

模板

小根堆
int h[N], size;//堆和堆的大小

void down(int u)
{
    int t = u;
    if (u * 2 <= size && h[u * 2] < h[t]) t = u * 2;
    if (u * 2 + 1 <= size && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
    if (u != t)
    {	
    	swap(h[u], h[t]);
        down(t);
    }
}

void up(int u)
{
    while (u / 2 && h[u] < h[u / 2])
    {
        swap(h[u], h[u / 2]);
        u >>= 1;
    }
}

// O(n)建堆
for (int i = n / 2; i; i -- ) down(i);
int h[N];//堆
int n,m;
void down(int x){//小根堆 下调
    int p = x*2;//左孩子下标
    while(p <= n){
        if(p + 1 <=n && h[p+1] < h[p]) p++;//找到子节点的最小值
        if(h[x]<=h[p]) return;//调整完毕
        swap(h[x],h[p]);
        x = p;
        p = x*2;
    }
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i = 1; i <= n; i++) scanf("%d",&h[i]);
    for(int i = n/2; i >=1; i--) down(i);//建堆,从最后一个叶子的父节点开始调整即可
    while(m--){
        printf("%d ",h[1]);//堆顶即为最小值
        swap(h[1],h[n]),n--,down(1);//删除堆顶
    }
    return 0;
}

10.归并排序

给定你一个长度为n的整数数列。

请你使用归并排序对这个数列按照从小到大进行排序。

并将排好序的数列按顺序输出。

思想

如名所示。
递之后,在归的过程中合并有序单元。

const int N = 1e5+10;
int q[N], tmp[N];//q为原数组 tmp为归并的辅助数组

void merge_sort(int q[], int l, int r) //将l~r从小到大排序
{
    if (l >= r) return;

    int mid = l + r >> 1;

    merge_sort(q, l, mid);
    merge_sort(q, mid + 1, r); //左右部分别排好序
	
	//合并左右两部分 ,k为待填充位置 每次选最小的去填
    //i为左部分的起始下标 j为右部边的起始下标 mid为左部分的边界
    int k = 0, i = l, j = mid + 1;
    while (i <= mid && j <= r)
        if (q[i] <= q[j]) tmp[k ++ ] = q[i ++ ]; //选择左部分的值去填的条件
        else tmp[k ++ ] = q[j ++ ];//否则只能选右半部分了
    while (i <= mid) tmp[k ++ ] = q[i ++ ];
    while (j <= r) tmp[k ++ ] = q[j ++ ];

    for (i = l, j = 0; i <= r; i ++, j ++ ) q[i] = tmp[j]; //辅助数组赋值给原数组
}

11.归并排序的应用-逆序对的数量

给定一个长度为n的整数数列,请你计算数列中的逆序对的数量。

逆序对的定义如下:对于数列的第 i 个和第 j 个元素,如果满足 i < j 且 a[i] > a[j],则其为一个逆序对;否则不是。

输入样例:

6

2 3 4 5 6 1

输出样例:

5

解释:逆序对分别为 (2,1)(3,1)(4,1)(5,1)(6,1)

思想

  1. 归并排序归的过程中每次合并左右两个有序区间,如果 q[i] > q[j] 则逆序,此时统计逆序对数量 ,则q[i~mid]的元素均大于q[j]构成逆序,右区间指针j后移,这保证右区间一个元素只会被统计一次。
  2. 不重。左右两个未合并区间在合并时统计逆序对数量,统计两个区间之间元素逆序对的数量,不统计自身区间元素逆序对数量,有序也为0,合并之后继续统计,所以不会重复。
  3. 不漏。合并过程中,对于任意一个元素都统计了其与后面所有元素的逆序对,所以没有遗漏。
int a[N], tmp[N];

LL merge_sort(int q[], int l, int r)//l~r从小到大排 排序过程中统计逆序对数量
{
    if (l >= r) return 0;

    int mid = l + r >> 1;

    LL res = merge_sort(q, l, mid) + merge_sort(q, mid + 1, r);//逆序对数量

    int k = 0, i = l, j = mid + 1;
    while (i <= mid && j <= r)
        if (q[i] <= q[j]) tmp[k ++ ] = q[i ++ ];
        else
        {
            res += mid - i + 1;        //此时q[i]>q[j] 逆序 统计逆序对数量  则q[i~mid]的元素均大于q[j]构成逆序
            tmp[k ++ ] = q[j ++ ];
        }
    while (i <= mid) tmp[k ++ ] = q[i ++ ];
    while (j <= r) tmp[k ++ ] = q[j ++ ];

    for (i = l, j = 0; i <= r; i ++, j ++ ) q[i] = tmp[j];

    return res;
}

12.基数排序

给你一个整数数组 nums,请你将该数组升序排列。

思想

使用十个桶0-9,把每个数从低位到高位根据位数放到相应的桶里,以此循环最大值的位数次。

只能排列正整数,因为遇到负号和小数点无法进行比较。

空间:O(r ) r个队列

最好:O(d(n+r)) d趟分配收集 一趟分配O(n)收集O(r )

最坏:O(d(n+r))

平均:O(d(n+r))

稳定性:是

void radixSort(int arr[]) {
    int _max = (*max_element(arr, arr+len));
        // 计算最大值的位数
        int maxDigits = 0; 
        while(_max) {
        maxDigits++;
        _max /= 10;
    }
    // 标记每个桶中存放的元素个数
    int bucketSum[10];
    memset(bucketSum, 0, sizeof(bucketSum));
    int div = 1; 
    // 第一维表示位数即0-9,第二维表示里面存放的值
    int res[10][1000];
    while(maxDigits--) {
        int digit;
        // 根据数组元素的位数将其放到相应的桶里,即分配
        for(int i=0; i<len; i++) {
            digit = arr[i] / div % 10;
            res[digit][bucketSum[digit]++] = arr[i];
        }
        // 把0-9桶里的数放回原数组后再进行下一个位数的计算,即收集
        int index = 0;
        for(int i=0; i<=9; i++) {
            for(int t=0; t<bucketSum[i]; t++) {
                arr[index++] = res[i][t];
            }
        }
        memset(bucketSum, 0, sizeof(bucketSum));
        div *= 10;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值