排序算法

  • 注:完全搬自wikipedia,仅用于总结之用
  • 冒泡排序

它重复地走访过要排序的数列,一次比较两个相邻元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

时间复杂度:(平均/最坏) O(n2)   需要O(n)额外空间

void bubble_sort(int *arr, int len) {
    int i, j, temp;
    for (i = 0; i < len - 1; i++)
    {
            for (j = 0; j < len - 1 - i; j++)
            if (arr[j] > arr[j + 1]) {
                temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
        }
    }
}    
  • 选择排序

首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

时间复杂度:(平均/最坏) O(n2)    键的交换次数:O(n)

void selection_sort(int *arr, int len) {
    int i, j, min, temp;
    for (i = 0; i < len - 1; i++) {
        min = i;
        for (j = i + 1; j < len; j++)
        {
       if (arr[min] > arr[j]) min = j; }//找出剩余未排序数组的最小值的位置
     temp
= arr[min]; arr[min] = arr[i]; arr[i] = temp; } }
  • 插入排序

使用了减治法。通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。基于递归思想。从底至上使用迭代,效率会更高。

(对数组A[0 ... n-1] 进行排序,可以假设较小的A[0 ... n-2] 排序的问题已经解决了,得到一个大小为n-1的有序数组,所需要考虑的问题就是如何将元素A[n-1]插入有n-1个元素的有序数组。可以按照从右到左扫描此有序数组,选择合适的位置插入。显然,这是归。 

图示:

                                                          

(署名:由Swfung8 - 自己的作品,CC BY-SA 3.0,https://commons.wikimedia.org/w/index.php?curid=14961606)

时间复杂度:(平均/最坏) O(n2

void insertion_sort(int *A, int len) {
    int i, j;
    int temp;
    for (i = 1; i < len; i++) {
        temp = A[i]; //與已排序的數逐一比較,大於temp時,該數向後移
        for (j = i - 1; j >= 0 && A[j] > temp; j--) //判决条件中A[j]>temp是终止条件
            A[j + 1] = A[j];
        A[j+1] = temp; //被排序数放到正确的位置
    }
}
  •  希尔排序

希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。希尔排序是非稳定排序算法,实质是分组插入排序。

希尔排序是基于插入排序的以下两点性质而提出改进方法的:

  • 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率
  • 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位

一个更好理解的希尔排序实现:将数组列在一个表中并对列排序(用插入排序)。重复这过程,不过每次用更长的列来进行。最后整个表就只有一列了。将数组转换至表是为了更好地理解这算法,算法本身仅仅对原数组进行排序(通过增加索引的步长,例如是用i += step_size而不是i++)。

void shellSort(int* a,int n){
    int gap,i,j;
    int tmp;
    for(gap = n>>1;gap > 0; gap >>= 1)
        for(i = gap;i < n; i++){
            tmp = a[i];
            for(j = i- gap;j >= 0 && a[j] > tmp;j -= gap)
                a[j + gap] = a[j];
            a[j + gap] = tmp;
        }
}
  • 归并排序

分治法:

  1) 将一个问题划分为同一类型的若干子问题,子问题最好规模相同。

  2) 对这些子问题求解(一般使用递归方法,但在问题规模足够小的时候,有时也会利用另一个算法)。

  3) 有必要的话,合并这些子问题的解,已得到原始问题的答案。

归并操作(merge),也叫归并算法,指的是将两个已经排序的序列合并成一个序列的操作。归并排序算法依赖归并操作。

void merge(int *a, int first, int mid, int last)
{
    int i = first, j = mid + 1;
    int m = mid, n = last;
    int k = 0;
    int* b = (int*)malloc((last-first + 1)* sizeof(int));//注意:元素个数为last-first+1,而不是last-first
    if (b ==NULL)
    {
        printf("malloc error!
");
        return 0;
    }
    while (i <= m && j <= n)
        b[k++] = (a[i] <= a[j]) ? a[i++] : a[j++];
    while (i <= m)
        b[k++] = a[i++];
    while (j <= n)
        b[k++] = a[j++];
    for (i = first,k = 0; i <= last; i++,k++)
        a[i] = b[k];//注意:每次迭代first都是变化的,这个时候需要将数组b中的元素代会数组a,注意i的起始值
    free(b);//释放堆上的变量
}  //合并两个有序的序列

归并排序主要有两类主要的变化形式,首先,算法可以自底而上合并数组的一个个元素对,然后再合并这些有序对,以此类推(如果元素数量不算2的幂,算法的效率也没有质的变化)。这就避免了使用堆栈处理递归调用时的时间和空间开销,效率更高些,但可读性差,又称为二路合并排序。其次,可以把数组划分为待排序的多个部分,再对它们进行递归排序,最后将其合并在一起。

迭代法

  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置
  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
  4. 重复步骤3直到某一指针到达序列尾
  5. 将另一序列剩下的所有元素直接复制到合并序列尾

空间复杂度:O(1)

递归法

  1. 将序列每相邻两个数字进行归并操作,形成floor(n/2)个序列,排序后每个序列包含两个元素
  2. 将上述序列再次归并,形成floor(n/4)个序列,每个序列包含四个元素
  3. 重复步骤2,直到所有元素排序完毕

空间复杂度:O(n) + O(logn)

 时间复杂度:(最好/平均/最坏) O(nlog2n) 

//递归法(自顶而下法)
void mergesort(int* a, int first, int last)
{
    if (first < last)
    {
        int mid = (first + last) / 2;
        mergesort(a, first, mid);    //左边有序  
        mergesort(a, mid + 1, last); //右边有序  
        merge(a, first, mid, last); //再将二个有序数列合并  
    }
}

void mergeSort(int* a, int n)
{
    mergesort(a, 0, n - 1);
}
//迭代法(自底而上法)
void
mergePass(int *a, int seg, int n) { int i = 0; // 归并seg长度的两个相邻子表 for (i = 0; i + 2 * seg - 1 < n; i = i + 2 * seg) { Merge(a, i, i + seg - 1, i + 2 * seg - 1); } // 余下两个子表,后者长度小于gap(考虑到奇数) if (i + gap - 1 < n) { merge(a, i, i + seg - 1, n - 1); } } void mergeSort(int* a) {   int seg;   for (seg = 1; seg < n; seg *= 2;) {   mergePass(a, seg, n); }

 参考blog:blog1 blog2

  •  快速排序

快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为两个子序列(sub-lists)。

步骤为:

  1. 从数列中挑出一个元素,称为"基准"(pivot),
  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
  3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。

快速排序是二叉查找树(二叉查找树)的一个空间最优化版本。不是循序地把数据项插入到一个明确的树中,而是由快速排序组织这些数据项到一个由递归调用所隐含的树中。这两个算法完全地产生相同的比较次数,但是顺序不同。对于排序算法的稳定性指标,原地分区版本的快速排序算法是不稳定的。其他变种是可以通过牺牲性能和空间来维护稳定性的。

时间复杂度:平均:O(nlogn) 最坏:O(n) 空间:O(logn)

同样分为迭代实现和递归实现。代码为迭代实现:

//递归法实现快排
void swap(int * x,int * y){
    int * t = *x;
    *x = *y;
    *y = t;
}
void quickSort(int* a,int start,int end){
    if(start >= end)
        return;
    int mid = a[end];
    int left = start,right = end -1;
    while(left < right)
    {
        while(a[left] < mid && left < right)//选取mid作为基准点
            left++;
        while (a[right] >= mid && left < right)//注意:是大于等于
            right--;
        swap(&a[left], &a[right]);
    }
    if(a[lest] >= a[end])
        swap(a+left,a+end);
    else
        left++;
    quickSort(a,start,left-1);//注意:选择的基准不在参与迭代排序
    quickSort(a,left+1,end);
}
void quick_sort(int *a,int n)
{
    quickSort(a,0,n-1);
}

参考blog:blog1

  •  堆排序

是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

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

二叉堆满足二个特性:

1.父结点的键值总是大于或等于(小于或等于)任何一个子节点的键值。

2.每个结点的左子树和右子树都是一个二叉堆(都是最大堆或最小堆)。

通常堆是通过一维数组来实现的。在数组起始位置为0的情形中:

  • 父节点i的左子节点在位置(2*i+1);
  • 父节点i的右子节点在位置(2*i+2);
  • 子节点i的父节点在位置floor((i-1)/2);

参考链接: blog1

原文地址:https://www.cnblogs.com/gardenofhu/p/5823790.html