排序算法总结


1.冒泡排序

1.思路

每次遍历整个数组,寻找一对逆序对,并将其交换,最后得到的数组肯定是有序的。

2.代码块
for(int i=1;i<=n;i++)
    for(int j=i+1;j<=n;j++) if(a[i]>a[j]) swap(a[i],a[j]);//每次两个一起比较,J在后面一个,按升序排列。

2.选择排序

1.思路

每次在数组里寻找最小的元素,将它移到首部,然后重复同样的操作,直到数组完全有序为止。

2.代码块
for(int i=1;i<=n;i++)
{
    int index=i;
    for(int j=i+1;j<=n;j++) if(a[j]<a[index]) index=j;
    swap(a[index],a[i]);
}

3.插入排序

1.思路

遍历整个数组,将当前遍历到的数插入到一个已经有序的数组中,并将该数组排序。

2.代码块
vector<int> vec;
for(int i=1;i<=n;i++)
{
    vec.push_back(a[i]);
    sort(vec.begin(),vec.end());
}

4.计数排序

1.思路

开一个可存储0~INF的数组,每搜索到一个值,数组中指向该值的下标的值加一,这
样可以完成去重(或直接调用STL中的unique函数)或排序的操作。在这四种算法中,前三种算法都是O(N^2)的,但计数排序没有一定的复杂度。

2.代码块
for(int i=1;i<=n;i++) b[a[i]]++;
for(int i=1;i<=n;i++)
    while(b[i]) {cout<<i<<' ';b[i]--;}

5.快速排序

1.思路

原理:
   快速排序,就是给基准数据找其正确索引位置的过程.
   如下图所示,假设最开始的基准数据为数组第一个元素23,则首先用一个临时变量去存储基准数据,即tmp=23;然后分别从数组的两端扫描数组,设两个指示标志:low指向起始位置,high指向末尾.
   在这里插入图片描述
   首先从后半部分开始,如果扫描到的值大于基准数据就让high减1,如果发现有元素比该基准数据的值小(如上图中18<=tmp),就将high位置的值赋值给low位置 ,结果如下:
   在这里插入图片描述
   然后开始从前往后扫描,如果扫描到的值小于基准数据就让low加1,如果发现有元素大于基准数据的值(如上图46=>tmp),就再将low位置的值赋值给high位置的值,指针移动并且数据交换后的结果如下:
   在这里插入图片描述
   然后再开始从后向前扫描,原理同上,发现上图11<=tmp,则将low位置的值赋值给high位置的值,结果如下:
   在这里插入图片描述
   然后再开始从前往后遍历,直到low=high结束循环,此时low或high的下标就是基准数据23在该数组中的正确索引位置.如下图所示.
   在这里插入图片描述
   这样一遍走下来,可以很清楚的知道,其实快速排序的本质就是把基准数大的都放在基准数的右边,把比基准数小的放在基准数的左边,这样就找到了该数据在数组中的正确位置.
  以后采用递归的方式分别对前半部分和后半部分排序,当前半部分和后半部分均有序时该数组就自然有序了。
  结论:①先从队尾开始向前扫描且当low < high时,如果a[high] > tmp,则high–,但如果a[high] < tmp,则将high的值赋值给low,即arr[low] = a[high],同时要转换数组扫描的方式,即需要从队首开始向队尾进行扫描了
  ②同理,当从队首开始向队尾进行扫描时,如果a[low] < tmp,则low++,但如果a[low] > tmp了,则就需要将low位置的值赋值给high位置,即arr[low] = arr[high],同时将数组扫描方式换为由队尾向队首进行扫描.
  ③不断重复①和②,知道low>=high时(其实是low=high),low或high的位置就是该基准数据在数组中的正确索引位置.
  思路:1、先从数列中取出一个数作为基准数

            2、分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边

            3、再对左右区间重复第二步,直到各区间只有一个数
2.代码块
//快速排序
void quick_sort(int s[], int l, int r)
{
    if (l < r)
    {
		//Swap(s[l], s[(l + r) / 2]); //将中间的这个数和第一个数交换 参见注1
        int i = l, j = r, x = s[l];
        while (i < j)
        {
            while(i < j && s[j] >= x) // 从右向左找第一个小于x的数
				j--;  
            if(i < j) 
				s[i++] = s[j];
			
            while(i < j && s[i] < x) // 从左向右找第一个大于等于x的数
				i++;  
            if(i < j) 
				s[j--] = s[i];
        }
        s[i] = x;
        quick_sort(s, l, i - 1); // 递归调用 
        quick_sort(s, i + 1, r);
    }
}

void quick_sort1(int s[], int l, int r)
{
	if (l < r)
    {
		int i = AdjustArray(s, l, r);//先成挖坑填数法调整s[]
		quick_sort1(s, l, i - 1); // 递归调用 
		quick_sort1(s, i + 1, r);
	}
}
//分治的代码 
package com.nrsc.sort;

public class QuickSort {
	public static void main(String[] args) {
		int[] arr = { 49, 38, 65, 97, 23, 22, 76, 1, 5, 8, 2, 0, -1, 22 };
		quickSort(arr, 0, arr.length - 1);
		System.out.println("排序后:");
		for (int i : arr) {
			System.out.println(i);
		}
	}

	private static void quickSort(int[] arr, int low, int high) {

		if (low < high) {
			// 找寻基准数据的正确索引
			int index = getIndex(arr, low, high);

			// 进行迭代对index之前和之后的数组进行相同的操作使整个数组变成有序
			quickSort(arr, 0, index - 1);
			quickSort(arr, index + 1, high);
		}

	}

	private static int getIndex(int[] arr, int low, int high) {
		// 基准数据
		int tmp = arr[low];
		while (low < high) {
			// 当队尾的元素大于等于基准数据时,向前挪动high指针
			while (low < high && arr[high] >= tmp) {
				high--;
			}
			// 如果队尾元素小于tmp了,需要将其赋值给low
			arr[low] = arr[high];
			// 当队首元素小于等于tmp时,向前挪动low指针
			while (low < high && arr[low] <= tmp) {
				low++;
			}
			// 当队首元素大于tmp时,需要将其赋值给high
			arr[high] = arr[low];

		}
		// 跳出循环时low和high相等,此时的low或high就是tmp的正确索引位置
		// 由原理部分可以很清楚的知道low位置的值并不是tmp,所以需要将tmp赋值给arr[low]
		arr[low] = tmp;
		return low; // 返回tmp的正确位置
	}
}

3.活学活用的思考

题目:在 O(n)的时间复杂度,O(1) 的空间复杂度内将只含有0,1,2三种数的数组排序

快速排序模板
时间复杂度 O(nlong(n))
空间复杂度 O(1)

#include <iostream>

using namespace std ;

const int N = 100010 ;

int a[N] ;
int n ;

void quick_sort(int a[],int l,int r){
    if(l>=r){
        return ; 
    }
    int x = a[l+r>>1],i=l-1,j=r+1 ;
    while(i<j){
        do i++ ; while(a[i]<x) ;
        do j-- ; while(a[j]>x) ;
        if(i<j) swap(a[i],a[j]) ;
    }
    quick_sort(a,l,j) ;
    quick_sort(a,j+1,r) ;
}


int main(){
    scanf("%d",&n) ;

    for(int i=0;i<n;i++){
        scanf("%d",&a[i]) ;
    }
    int l=0,r=n-1 ;

    quick_sort(a,0,n-1) ;

    for(int i=0;i<n;i++){
        printf("%d ",a[i]) ;
    }
    return 0 ;
}

引申荷兰国旗问题
正确解法: 思路也是分治,将数组中大于分割值的数放右边,小于的放左边,等于的放中间

时间复杂度 O(n)
空间复杂度 O(1)

void quick_sort(int a[],int l,int r){
    int i = l-1 ;//左边界
    int j = r+1 ;//右边界
    while(l<j){
        if(a[l]<1){
            swap(a[++i],a[l++]) ;
        }else if(a[l]>1){
            swap(a[l],a[--j]) ;
        }else{
            l++ ;
        }
    }
}

彩虹排序:
给定一个有n个对象(包括k种不同的颜色,并按照1到k进行编号)的数组,将对象进行分类使相同颜色的对象相邻,并按照1,2,…k的顺序进行排序。
样例
给出colors=[3, 2, 2, 1, 4],k=4, 你的代码应该在原地操作使得数组变成[1, 2, 2, 3, 4]

题目:
一个相当直接的解决方案是使用计数排序扫描2遍的算法。这样你会花费O(k)的额外空间。你否能在不使用额外空间的情况下完成?
思路:算法时间复杂度要求到O(nlogk),k为颜色个数。

解题思路:

quickSort的思想在于partition进行分割,mergeSort的思想在于直接取中间(这里表现为取中间大小的数),分为左右两个相等长度的部分。区别在于partition的判定条件变为了中间大小的元素而不是中间位置的元素,因此等号的取值可以只去一边也不会有影响。
rainbowSort实现的是将colors数组的索引范围start到end位置排序,排序的大小范围是colorFrom到coloTo

public class Solution {
    /**
     * @param colors: A list of integer
     * @param k: An integer
     * @return: nothing
     */
    public void sortColors2(int[] colors, int k) {
        // write your code here
        //O(nlogk), the best algorithm based on comparing
        //算法时间复杂度要求到O(nlogk),k为颜色个数
        //这种算法的思想类似与quickSort与mergeSort结合
        //quickSort的思想在于partition进行分割
        //mergeSort的思想在于直接取中间(这里表现为取中间大小的数),分为左右两个相等长度的部分
        //区别在于partition的判定条件变为了中间大小的元素而不是中间位置的元素
        //因此等号的取值可以只去一边也不会有影响
        if (colors == null || colors.length == 0) {
            return;
        }
        rainbowSort(colors, 0, colors.length - 1, 1, k);
    }
    //将colors数组的索引范围start到end位置排序
    //排序的大小范围是colorFrom到colorTo
    private void rainbowSort(int[] colors,
                            int start,
                            int end,
                            int colorFrom,
                            int colorTo) {
        if (colorFrom == colorTo) {
            return;
        }
        if (start == end) {
            return;
        }
        //区别在于partition的判定条件变为了中间大小的元素而不是中间位置的元素
        int colorMid = (colorFrom + colorTo) / 2;
        int left = start, right = end;
        while (left <= right) {
            //因此等号的取值可以只去一边也不会有影响
            while (left <= right && colors[left] <= colorMid) {
                left++;
            }
            while (left <= right && colors[right] > colorMid) {
                right--;
            }
            if (left <= right) {
                int temp = colors[left];
                colors[left] = colors[right];
                colors[right] = temp;
                left++;
                right--;
            }
        }
        rainbowSort(colors, start, right, colorFrom, colorMid);
        rainbowSort(colors, left, end, colorMid + 1, colorTo);
    }
}

Kicksort排序
在Quicksort算法中,需要从列表中选择一个关键数,让其他数与关键数作比较,并根据该数与关键数的比较结果将它放入两个新列表中的一个,然后在每个新列表中进行递归排序。

但是,该算法可能会出现一种特殊情况,即选择了一个特殊的关键数,导致其他所有数都被分到同一个新列表中(都比关键数大或都比关键数小),这违背了分而治之策略的目的。

我们将这种关键数称为最坏情况的关键数。

为了避免这个问题,我们创建了自己的变体Kicksort。

有人告诉我们,使用位于序列中间的数作为关键数很不错,所以我们的算法如下工作:

Kicksort(A): // A is a 0-indexed array with E elements
    If E ≤ 1, return A.
    Otherwise:
      Create empty new lists B and C.
      Choose A[floor((E-1)/2)] as the pivot P.
      For i = 0 to E-1, except for i = floor((E-1)/2):
        If A[i] ≤ P, append it to B.
        Otherwise, append it to C.
    Return the list Kicksort(B) + P + Kicksort(C).

为了验证这一方法,我们生成了很多由数字1到N随机排列形成的数字列表,并用Kicksort排序法进行排序。

不幸的是,看起来Kicksort仍然有与Quicksort相同的问题:每个关键数都可能成为最坏情况的关键数!

例如,考虑列表1 4 3 2,Kicksort将选择4作为关键数,这将使得1 3 2全部被分配到同一个新列表中。

然后,当在列表1 3 2上调用Kicksort时,它将选择3作为关键数,这将使得1 2全部被分配到同一个新列表中。

最后,它将从列表1 2中选择1,而另一个值2当然被分配到了一个新列表中。

在每一个步骤中,算法都会选择最坏情况的关键数。 (请注意,当在具有0或1个元素的列表上调用Kicksort时,它根本不会选择关键数。)

请帮助我们进一步调查,给定由数字1到N构成的列表,确定Kicksort是否只选择最坏情况的关键数。

输入格式
第一行包含整数T,表示共有T组测试数据。

每组数据占两行,第一行包含整数N,表示列表长度。

第二行包含N个整数,它们是1到N这N个数字的一种排列。

输出格式
每组数据输出一个结果,每个结果占一行。

结果表示为“Case #x: y”,其中x是组别编号(从1开始),如果Kicksort在排序此列表时仅选择最坏情况的关键数,则y为YES,否则为NO。

数据范围
1≤T≤100,
2≤N≤10000
输入样例:
4
4
1 4 3 2
4
2 1 3 4
2
2 1
3
1 2 3
输出样例:
Case #1: YES
Case #2: NO
Case #3: YES
Case #4: NO
样例解释
样例#1是问题陈述中描述的样例。

在样例#2中,我们的第一个关键数将是1,这是最坏情况的关键数,因为它导致所有其他值2 3 4最终被分配到同一个新列表中。 但是,列表2 3 4上调用Kicksort时将选择3作为关键数。 这不是最坏情况的关键数,因为它将2放在一个新列表中,而将4放在了另一个新列表中。

在样例#3中,Kicksort将从选择最坏情况的关键数2开始,并且没有其他关键数供其选择。

在样例#4中,Kicksort将从选择2开始,这不是最坏情况的关键数。

难度: 简单
时/空限制: 1s / 64MB
总通过数: 3
总尝试数: 3
来源: Google Kickstart2017 Round F Problem A

6.超快速排序

在这个问题中,您必须分析特定的排序算法----超快速排序。

该算法通过交换两个相邻的序列元素来处理n个不同整数的序列,直到序列按升序排序。

对于输入序列9 1 0 5 4,超快速排序生成输出0 1 4 5 9。

您的任务是确定超快速排序需要执行多少交换操作才能对给定的输入序列进行排序。

输入格式
输入包括一些测试用例。

每个测试用例的第一行输入整数n,代表该用例中输入序列的长度。

接下来n行每行输入一个整数ai,代表用例中输入序列的具体数据,第i行的数据代表序列中第i个数。

当输入用例中包含的输入序列长度为0时,输入终止,该序列无需处理。

输出格式
对于每个需要处理的输入序列,输出一个整数op,代表对给定输入序列进行排序所需的最小交换操作数,每个整数占一行。

数据范围
0≤N<500000,
0≤ai≤999999999
输入样例:
5
9
1
0
5
4
3
1
2
3
0
输出样例:
6
0
思路:
归并排序求逆序对
首先发现题目就是在模拟冒泡排序,而交换的次数,就是冒泡排序的交换次数就是我们的逆序对个数,至于求逆序对最快的方法,就是归并排序。
还要注意这里一定要用Longlong,不然的话就会WA,因为最大值可能为n∗n

#include <bits/stdc++.h>
using namespace std;
const int N=501000;
#define ll long long
ll n,m,i,j,k,a[N],b[N],cnt;
void merge(ll a[],ll l,ll r)
{
    if (r-l<1)
        return ;
    ll mid=(l+r)>>1;
    merge(a,l,mid);
    merge(a,mid+1,r);
    ll i=l,j=mid+1;
    for (ll k=l;k<=r;k++)
    {
        if (j>r || i<=mid && a[i]<=a[j])
            b[k]=a[i++];
        else
        {
            cnt+=mid-i+1;//[mid,i]这个区间的每一个数都可以和当前数构成逆序对。
            b[k]=a[j++];
        }
    }
    for (ll k=l;k<=r;k++)
        a[k]=b[k];
}
int main()
{
    ios::sync_with_stdio(false);//是一个固定的语法。加上这句话后,scanf,printfscanf,printf不可以使用,但是cin,cout的读入速度直线飙升。使用上cin.tie(0),cout.tie(0)上cin.tie(0),cout.tie(0) 。读入速度和scanfscanf基本上一样了
    while(cin>>n && n)
    {
        for (i=1;i<=n;i++)
            cin>>a[i];
        cnt=0;
        merge(a,1,n);
        cout<<cnt<<endl;
    }
    return 0;
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值