基础排序算法总结

来源:互联网 发布:telent的端口号 编辑:程序博客网 时间:2024/06/14 10:59

八中算法的稳定性问题分析

通俗的讲就是能保证排序前2个相等的数其在序列的前后位置顺序和排序后他们两个的前后位置顺序相同。简化形式为:如果Ai=Aj,Ai原来在位置前,排序后Ai还是要在Aj位置前。稳定排序:冒泡排序、插入排序、归并排序和基数排序;不稳定排序:选择排序、快速排序、希尔排序、堆排序
稳定性的好处:排序算法如果是稳定的,那么从一个健上排序,然后再从另一个健上排序,第一个排排序的结果可以为第二个健排序苏勇。基数排序就是这样,先按低位排序,逐次按高位排序,低位相同的元素其顺序在高位也相同时是不会改变的。

冒泡排序

冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,我想你是不会再无聊地把他们俩交换一下的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。

template<typename T> void bubble_sort(T arr[], int len) {    int i, j;    for (i = 0; i < len - 1; i++)        for (j = 0; j < len - 1 - i; j++)            if (arr[j] > arr[j + 1])                swap(arr[j], arr[j + 1]);}

1.最坏时间复杂度 O(n2)
2. 最优时间复杂度 O(n)
3. 平均时间复杂度 O(n2)
4. 空间复杂度 :总共O(n),需要辅助空间O(1)
在最坏的情况下冒泡排序需要O(n2)次交换,而插入排序只需要最多O(n)次交换。可以通过在内部第一次循环时,使用一个旗标来表示有无需要交换的可能,可以把最坏情况下的复杂度将到O(n),在这种情况下,排序好的数列就不用再交换了。
一种变形为鸡尾酒排序:也就是定向冒泡排序,以双向在序列中进行排序。不同:不同的地方在于从低到高然后从高到低,而冒泡排序则仅从低到高去比较序列里的每个元素。他可以得到比冒泡排序稍微好一点的效能,原因是冒泡排序只从一个方向进行比对(由低到高),每次循环只移动一个项目。

    void cocktail_sort(int arr[], int len){        int j, left = 0, right  = len - 1;        while(left < right){            for(j = left; j < right; j++){                if(arr[j] > arr[j+1])                    swap(arr[j],arr[j+1]);            }            right--;            for(j = right; j > left; j--)                if(arr[j-1] > arr[j])                    swap(arr[j-1],arr[j]);            left++;        }    }

鸡尾酒排序最糟或是平均所花费的次数都是 O(n2),但如果序列在一开始已经大部分排序过的话,会接近 O(n)

选择排序

选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n - 1个元素,第n个元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果当前元素比一个元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。比较拗口,举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法。
原理:搜先在未排序的序列中找到最小(大)元素,放在排序序列的起始位置,然后再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的尾部。
优点:主要与数据移动有关,如果某个元素位于正确的最终位置上,它不会被移动。选择排序每次交换一对元素,他们当中至少有一个将被移动到其最终位置上,因此对n个元素的表进行排序总共进行最多n-1次交换。在所有完全依靠交换区移动元素的排序算法中,选择排序属于非常好的一种。
enter image description here

template<typename T> void selection_sort(std::vector<T>& arr) {    for (int i = 0; i < arr.size() - 1; i++) {        int min = i;        for (int j = i + 1; j < arr.size(); j++)            if (arr[j] < arr[min])                min = j;        std::swap(arr[i], arr[min]);    }}

1.最坏时间复杂度 O(n2)
2. 最优时间复杂度 O(n2)
3. 平均时间复杂度 O(n2)
4. 空间复杂度 :总共O(n),需要辅助空间O(1)
选择排序的交换操作介于0(n1)次之间。选择排序的比较操作为 n(n1)/2次之间。选择排序的赋值操作介于03(n1)次之间。
比较次数O(n2),比较次数与关键字的初始状态无关,总的比较次数 N=(n1)+(n2)+...+1=n(n1)/2。交换次数O(n),最好情况是,已经有序,交换0次;最坏情况是,逆序,交换 n1次。交换次数比冒泡排序较少,由于交换所需CPU时间比比较所需的CPU时间多,n值较小时,选择排序比冒泡排序快。原地操作几乎是选择排序的唯一优点,当空间复杂度要求较高时,可以考虑选择排序;实际适用的场合非常罕见。

插入排序

插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。
原理:通过构建有序序列,对于未排序数据,在已排序序列中以后向前扫描,找到相应的位置并插入,插入排序在实现上,通常采用inplace排序(只需要用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后移位,为最新元素提供插入空间。
描述
1.从第一个元素开始,该元素可以认为已经被排序
2.取出下一个元素,在已经排序的元素序列中从后向前扫描
3.如果该元素(已排序)大于新元素,将该元素移到下一位置
4.重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
5.将新元素插入到该位置后
6.重复步骤2~5
如果比较操作的代价比交换操作大的话,可以采用二分查找法来减少比较操作的数目。该算法可以认为是插入排序的一个变种,称为二分查找插入排序。
enter image description here

void insertion_sort(int arr[], int len) {    for (int i = 1, j = 0 ; i < len; ++i)//对比len - 1次,直到最后只剩一个数    {        int temp = arr[i];  //temp为关键数        for (j = i - 1; j >= 0 && arr[j] > temp; --j)//对比关键数前的所有数            arr[j + 1] = arr[j];    //找到比关键数大的数值,插入到关键数后面    arr[j + 1] = temp;     //插入关键数    }}

1.最坏时间复杂度 O(n2)
2. 最优时间复杂度 O(n)
3. 平均时间复杂度 O(n2)
4. 空间复杂度 :总共O(n),需要辅助空间O(1)
如果目标是把n个元素的序列升序排列,那么采用插入排序存在最好情况和最坏情况。最好情况就是,序列已经是升序排列了,在这种情况下,需要进行的比较操作需(n1)次即可。最坏情况就是,序列是降序排列,那么此时需要进行的比较共有 0.5n(n1)次。插入排序的赋值操作是比较操作的次数加上(n1)次。平均来说插入排序算法复杂度为 O(n2)。因而,插入排序不适合对于数据量比较大的排序应用。但是,如果需要排序的数据量很小,例如,量级小于千;或者若已知输入元素大致上按照顺序排列,那么插入排序还是一个不错的选择。 插入排序在工业级库中也有着广泛的应用,在STL的sort算法和stdlib的qsort算法中,都将插入排序作为快速排序的补充,用于少量元素的排序(通常为8个或以下)。

二分查找插入排序

原理:是直接插入排序的一个变种,区别是:在有序区中查找新元素插入位置时,为了减少元素比较次数提高效率,采用二分查找算法进行插入位置的确定。
实现
设数组为a[0…n]。
1. 将原序列分成有序区和无序区。a[0…i-1]为有序区,a[i…n] 为无序区。(i从1开始)
2. 从无序区中取出第一个元素,即a[i],使用二分查找算法在有序区中查找要插入的位置索引j。
3. 将a[j]到a[i-1]的元素后移,并将a[i]赋值给a[j]。
4. 重复步骤2~3,直到无序区元素为0。

    /*二分查找函数,返回插入下标*/template <typename T>int BinarySearch(T array[], int start, int end, T k){    while (start <= end)    {        int middle = (start + end) / 2;        int middleData = array[middle];        if (middleData > k)        {            end = middle - 1;        }        else            start = middle + 1;    }    return start;}//二叉查找插入排序template <typename T>void InsertSort(T array[], int length){    if (array == nullptr || length < 0)        return;    int i, j;    for (i = 1; i < length; i++)    {        if (array[i]<array[i - 1])        {            int temp = array[i];            int insertIndex = BinarySearch(array, 0,i, array[i]);//使用二分查找在有序序列中进行查找,获取插入下标            for (j = i - 1; j>=insertIndex; j--) //移动元素            {                array[j + 1] = array[j];               }                   array[insertIndex] = temp;    //插入元素        }    }}
  1. 时间复杂度:O(n^2):二分查找插入位置,因为不是查找相等值,而是基于比较查插入合适的位置,所以必须查到最后一个元素才知道插入位置。二分查找最坏时间复杂度:当2^X>=n时,查询结束,所以查询的次数就为x,而x等于log2n(以2为底,n的对数)。即O(log2n);所以,二分查找排序比较次数为:x=log2n;二分查找插入排序耗时的操作有:比较 + 后移赋值。时间复杂度如下:
  2. 最好情况:查找的位置是有序区的最后一位后面一位,则无须进行后移赋值操作,其比较次数为:log2n 。即O(log2n)
  3. 最坏情况:查找的位置是有序区的第一个位置,则需要的比较次数为:log2n,需要的赋值操作次数为n(n-1)/2加上 (n-1) 次。即O(n^2)
  4. 渐进时间复杂度(平均时间复杂度):O(n^2)
  5. 空间复杂度:O(1)
    从实现原理可知,二分查找插入排序是在原输入数组上进行后移赋值操作的(称“就地排序”),所需开辟的辅助空间跟输入数组规模无关,所以空间复杂度为:O(1)
    (三)稳定性
    二分查找排序是稳定的,不会改变相同元素的相对顺序。

快速排序

快速排序有两个方向,左边的i下标一直往右走,当a[i] <= a[center_index],其中center_index是中枢元素的数组下标,一般取为数组第0个元素。而右边的j下标一直往左走,当a[j] > a[center_index]。如果i和j都走不动了,i <= j,交换a[i]和a[j],重复上面的过程,直到i > j。 交换a[j]和a[center_index],完成一趟快速排序。在中枢元素和a[j]交换的时候,很有可能把前面的元素的稳定性打乱,比如序列为5 3 3 4 3 8 9 10 11,现在中枢元素5和3(第5个元素,下标从1开始计)交换就会把元素3的稳定性打乱,所以快速排序是一个不稳定的排序算法,不稳定发生在中枢元素和a[j] 交换的时刻。
原理:划分交换排序,使用分治法策略,把一个序列分为两个子序列。
步骤
1.从数列中挑选一个元素,作为“基数”(pivot)
2.重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆放在基准后面(相同的数可以到任何一边)。在这个分区结束后,该基准就处于数列的中间位置,这个称为分区(partition)操作。
3.递归的把小于基准值元素的子序列和大于基准值元素的子序列排序
递归到最底部时,数列的大小是零或一,也就是已经排序好了。
enter image description here

function quicksort(q)    var list less, pivotList, greater    if lenght(q) <= 1{        return q;    }else{        select a pivot value pivot from q        for each x in q except the pivot element            if x < pivot then add x to less            if x >= pivot then add x to greater        add pivot to pivotList        return concatenate(quicksort(less),pivot,quicksort(greater))    }

**上述版本的缺点是,它需要O(n)的额外存储空间,也就和归并排序一样不好。额外需要的内存空间,实际上的实现,也会极度影响速度和缓存的性能。有一个比较复杂使用原地分区算法的版本。且在好的基准选择上,平均可以达到O(logn)空间的使用复杂度

function partition(a, left, right, pivotIndex)     pivotValue := a[pivotIndex]     swap(a[pivotIndex], a[right]) // 把pivot移到結尾     storeIndex := left     for i from left to right-1         if a[i] <= pivotValue             swap(a[storeIndex], a[i])             storeIndex := storeIndex + 1     swap(a[right], a[storeIndex]) // 把pivot移到它最後的地方     return storeIndex

它分区了标示为”左边(left)”和”右边(right)”的序列部分,借由移动小于a[pivotIndex]的所有元素到子序列的开头,留下所有大于或等于的元素接在他们后面。在这个过程它也为基准元素找寻最后摆放的位置,也就是它回传的值。它暂时地把基准元素移到子序列的结尾,而不会被前述方式影响到。由于算法只使用交换,因此最后的数列与原先的数列拥有一样的元素。要注意的是,一个元素在到达它的最后位置前,可能会被交换很多次。

procedure quicksort(a, left, right)     if right > left         select a pivot value a[pivotIndex]         pivotNewIndex := partition(a, left, right, pivotIndex)         quicksort(a, left, pivotNewIndex-1)         quicksort(a, pivotNewIndex+1, right)
//迭代法typedef struct _Range {    int start, end;} Range;Range new_Range(int s, int e) {    Range r;    r.start = s;    r.end = e;    return r;}void swap(int *x, int *y) {    int t = *x;    *x = *y;    *y = t;}void quick_sort(int arr[], const int len) {    if (len <= 0)        return;     Range r[len];    int p = 0;    r[p++] = new_Range(0, len - 1);    while (p) {        Range range = r[--p];        if (range.start >= range.end)            continue;        int mid = arr[range.end];        int left = range.start, right = range.end - 1;        while (left < right) {            while (arr[left] < mid && left < right)                left++;            while (arr[right] >= mid && left < right)                right--;            swap(&arr[left], &arr[right]);        }        if (arr[left] >= arr[range.end])            swap(&arr[left], &arr[range.end]);        else            left++;        r[p++] = new_Range(range.start, left - 1);        r[p++] = new_Range(left + 1, range.end);    }}
//递归法void swap(int *x, int *y) {    int t = *x;    *x = *y;    *y = t;}void quick_sort_recursive(int arr[], int start, int end) {    if (start >= end)        return;    int mid = arr[end];    int left = start, right = end - 1;    while (left < right) {        while (arr[left] < mid && left < right)            left++;        while (arr[right] >= mid && left < right)            right--;        swap(&arr[left], &arr[right]);    }    if (arr[left] >= arr[end])        swap(&arr[left], &arr[end]);    else        left++;    if (left) {        quick_sort_recursive(arr, start, left - 1);    }    quick_sort_recursive(arr, left + 1, end);}void quick_sort(int arr[], int len) {    quick_sort_recursive(arr, 0, len - 1);}

1.最坏时间复杂度 O(n2)
2. 最优时间复杂度 O(nlogn)
3. 平均时间复杂度 O(nlogn)
4. 空间复杂度 :根据实现的方式不同而不同

归并排序

归并排序是把序列递归地分成短序列,递归出口是短序列只有1个元素(认为直接有序)或者2个序列(1次比较和交换),然后把各个有序的段序列合并成一个有序的长序列,不断合并直到原序列全部排好序。可以发现,在1个或2个元素时,1个元素不会交换,2个元素如果大小相等也没有人故意交换,这不会破坏稳定性。那么,在短的有序序列合并的过程中,稳定是是否受到破坏?没有,合并过程中我们可以保证如果两个当前元素相等时,我们把处在前面的序列的元素保存在结果序列的前面,这样就保证了稳定性。所以,归并排序也是稳定的排序算法。
原理:是创建在归并操作上的一种有效的排序算法,采用分治法的思想。将两个已经排序的序列合并成一个序列的操作,依赖于归并操作。
这里写图片描述
步骤
1迭代法:
1.申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
2.设定两个指针,最初位置分别为两个已经排序序列的起始位置
3.比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
4.重复步骤3直到某一指针到达序列尾
5.将另一序列剩下的所有元素直接复制到合并序列尾
2递归发:
将序列每相邻两个数字进行归并操作,形成floor(n/2)个序列,排序后每个序列包含两个元素
将上述序列再次归并,形成 floor(n/4)个序列,每个序列包含四个元素
重复步骤2,直到所有元素排序完毕

//迭代法int min(int x, int y) {    return x < y ? x : y;}void merge_sort(int arr[], int len) {    int* a = arr;    int* b = (int*) malloc(len * sizeof(int));    int seg, start;    for (seg = 1; seg < len; seg += seg) {        for (start = 0; start < len; start += seg + seg) {            int low = start, mid = min(start + seg, len), high = min(start + seg + seg, len);            int k = low;            int start1 = low, end1 = mid;            int start2 = mid, end2 = high;            while (start1 < end1 && start2 < end2)                b[k++] = a[start1] < a[start2] ? a[start1++] : a[start2++];            while (start1 < end1)                b[k++] = a[start1++];            while (start2 < end2)                b[k++] = a[start2++];        }        int* temp = a;        a = b;        b = temp;    }    if (a != arr) {        int i;        for (i = 0; i < len; i++)            b[i] = a[i];        b = a;    }    free(b);}
//递归法void merge_sort_recursive(int arr[], int reg[], int start, int end) {    if (start >= end)        return;    int len = end - start, mid = (len >> 1) + start;    int start1 = start, end1 = mid;    int start2 = mid + 1, end2 = end;    merge_sort_recursive(arr, reg, start1, end1);    merge_sort_recursive(arr, reg, start2, end2);    int k = start;    while (start1 <= end1 && start2 <= end2)        reg[k++] = arr[start1] < arr[start2] ? arr[start1++] : arr[start2++];    while (start1 <= end1)        reg[k++] = arr[start1++];    while (start2 <= end2)        reg[k++] = arr[start2++];    for (k = start; k <= end; k++)        arr[k] = reg[k];}void merge_sort(int arr[], const int len) {    int reg[len];    merge_sort_recursive(arr, reg, 0, len - 1);}

1.最坏时间复杂度 O(nlogn)
2. 最优时间复杂度 O(n)
3. 平均时间复杂度 O(nlogn)
4. 空间复杂度 :O(n)

桶排序

原理:将数组分到有限数量的桶里,每个桶在个别排序(有可能在使用别的排序算法或是以递归方式继续使用桶排序进行排序)。
步骤
1.设置一个定量的数组当作空桶子。
2.寻访序列,并且把项目一个一个放到对应的桶子去。
3.对每个不是空的桶子进行排序。
4.从不是空的桶子里把项目再放回原来的序列中。
动态演示桶排序

//伪代码function bucket-sort(array, n) is  buckets ← new array of n empty lists  for i = 0 to (length(array)-1) do    insert array[i] into buckets[msbits(array[i], k)]  for i = 0 to n - 1 do    next-sort(buckets[i])  return the concatenation of buckets[0], ..., buckets[n-1]
#include<iterator>#include<iostream>#include<vector>using namespace std;const int BUCKET_NUM = 10;struct ListNode{    explicit ListNode(int i=0):mData(i),mNext(NULL){}    ListNode* mNext;    int mData;};ListNode* insert(ListNode* head,int val){    ListNode dummyNode;    ListNode *newNode = new ListNode(val);    ListNode *pre,*curr;    dummyNode.mNext = head;    pre = &dummyNode;    curr = head;    while(NULL!=curr && curr->mData<=val){        pre = curr;        curr = curr->mNext;    }    newNode->mNext = curr;    pre->mNext = newNode;    return dummyNode.mNext;}ListNode* Merge(ListNode *head1,ListNode *head2){    ListNode dummyNode;    ListNode *dummy = &dummyNode;    while(NULL!=head1 && NULL!=head2){        if(head1->mData <= head2->mData){            dummy->mNext = head1;            head1 = head1->mNext;        }else{            dummy->mNext = head2;            head2 = head2->mNext;        }        dummy = dummy->mNext;    }    if(NULL!=head1) dummy->mNext = head1;    if(NULL!=head2) dummy->mNext = head2;    return dummyNode.mNext;}void BucketSort(int n,int arr[]){    vector<ListNode*> buckets(BUCKET_NUM,(ListNode*)(0));    for(int i=0;i<n;++i){        int index = arr[i]/BUCKET_NUM;        ListNode *head = buckets.at(index);        buckets.at(index) = insert(head,arr[i]);    }    ListNode *head = buckets.at(0);    for(int i=1;i<BUCKET_NUM;++i){        head = Merge(head,buckets.at(i));    }    for(int i=0;i<n;++i){        arr[i] = head->mData;        head = head->mNext;    }}

1.最坏时间复杂度 O(n2)
2. 最优时间复杂度 O(n+k)
3. 平均时间复杂度 O(n+k)
4. 空间复杂度 :O(nk)

基数排序

基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序,最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以其是稳定的排序算法。
原理:类似桶排序,这里总是需要10个桶,多次使用
步骤:首先以个位数的值进行装桶,即个位数为1则放入1号桶,为9则放入9号桶,暂时忽视十位数
例如:
待排序数组[62,14,59,88,16]简单点五个数字
分配10个桶,桶编号为0-9,以个位数数字为桶编号依次入桶,变成下边这样
| 0 | 0 | 62 | 0 | 14 | 0 | 16 | 0 | 88 | 59 |
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |桶编号
将桶里的数字顺序取出来,
输出结果:[62,14,16,88,59]
再次入桶,不过这次以十位数的数字为准,进入相应的桶,变成下边这样:
由于前边做了个位数的排序,所以当十位数相等时,个位数字是由小到大的顺序入桶的,就是说,入完桶还是有序
| 0 | 14,16 | 0 | 0 | 0 | 59 | 62 | 0 | 88 | 0 |
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |桶编号
因为没有大过100的数字,没有百位数,所以到这排序完毕,顺序取出即可
最后输出结果:[14,16,59,62,88]

int maxbit(int data[], int n) //辅助函数,求数据的最大位数{    int maxData = data[0];      ///< 最大数    /// 先求出最大数,再求其位数,这样有原先依次每个数判断其位数,稍微优化点。    for (int i = 1; i < n; ++i)    {        if (maxData < data[i])            maxData = data[i];    }    int d = 1;    int p = 10;    while (maxData >= p)    {        //p *= 10; // Maybe overflow        maxData /= 10;        ++d;    }    return d;/*    int d = 1; //保存最大的位数    int p = 10;    for(int i = 0; i < n; ++i)    {        while(data[i] >= p)        {            p *= 10;            ++d;        }    }    return d;*/}void radixsort(int data[], int n) //基数排序{    int d = maxbit(data, n);    int *tmp = new int[n];    int *count = new int[10]; //计数器    int i, j, k;    int radix = 1;    for(i = 1; i <= d; i++) //进行d次排序    {        for(j = 0; j < 10; j++)            count[j] = 0; //每次分配前清空计数器        for(j = 0; j < n; j++)        {            k = (data[j] / radix) % 10; //统计每个桶中的记录数            count[k]++;        }        for(j = 1; j < 10; j++)            count[j] = count[j - 1] + count[j]; //将tmp中的位置依次分配给每个桶        for(j = n - 1; j >= 0; j--) //将所有桶中记录依次收集到tmp中        {            k = (data[j] / radix) % 10;            tmp[count[k] - 1] = data[j];            count[k]--;        }        for(j = 0; j < n; j++) //将临时数组的内容复制到data中            data[j] = tmp[j];        radix = radix * 10;    }    delete []tmp;    delete []count;}

希尔排序

希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小, 插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比O(n^2)好一些。由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。
原理:基于插入排序,使数组中任意间隔为h的元素都是有序的,即将全部元素分为h个区域,使用插入排序。
这里写图片描述

template<typename T>void shell_sort(T arr[], int len) {    int gap, i, j;    T temp;    for (gap = len >> 1; gap > 0; gap >>= 1)        for (i = gap; i < len; i++) {            temp = arr[i];            for (j = i - gap; j >= 0 && arr[j] > temp; j -= gap)                arr[j + gap] = arr[j];            arr[j + gap] = temp;        }}

1.最坏时间复杂度 根据步长序列的不同而不同。已知最好的: O(nlog2n)
2.最优时间复杂度 O(n)
3.平均时间复杂度 根据步长序列的不同而不同。
4.空间复杂度:O(n)

堆排序

我们知道堆的结构是节点i的孩子为2 * i和2 * i + 1节点,大顶堆要求父节点大于等于其2个子节点,小顶堆要求父节点小于等于其2个子节点。在一个长为n 的序列,堆排序的过程是从第n / 2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n / 2 - 1, n / 2 - 2, … 1这些个父节点选择元素时,就会破坏稳定性。有可能第n / 2个父节点交换把后面一个元素交换过去了,而第n / 2 - 1个父节点把后面一个相同的元素没 有交换,那么这2个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法。
原理:利用堆这种数据结构所设计的排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质,即子节点的键值或索引总是小于(或者大于)它的父节点。
通常堆是通过一维数组来实现的。在数组起始位置为0的情形中:
父节点i的左子节点在位置(2*i+1);
父节点i的右子节点在位置(2*i+2);
子节点i的父节点在位置floor((i-1)/2);
在堆的数据结构中,堆中的最大值总是位于根节点(在优先队列中使用堆的话堆中的最小值位于根节点)。堆中定义以下几种操作:
最大堆调整(Max_Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点
创建最大堆(Build_Max_Heap):将堆所有数据重新排序
堆排序(HeapSort):移除位在第一个数据的根节点,并做最大堆调整的递归运算
这里写图片描述

#include <stdio.h>#include <stdlib.h>void swap(int* a, int* b) {    int temp = *b;    *b = *a;    *a = temp;}void max_heapify(int arr[], int start, int end) {    //建立父節點指標和子節點指標    int dad = start;    int son = dad * 2 + 1;    while (son <= end) { //若子節點指標在範圍內才做比較        if (son + 1 <= end && arr[son] < arr[son + 1]) //先比較兩個子節點大小,選擇最大的            son++;        if (arr[dad] > arr[son]) //如果父節點大於子節點代表調整完畢,直接跳出函數            return;        else { //否則交換父子內容再繼續子節點和孫節點比較            swap(&arr[dad], &arr[son]);            dad = son;            son = dad * 2 + 1;        }    }}void heap_sort(int arr[], int len) {    int i;    //初始化,i從最後一個父節點開始調整    for (i = len / 2 - 1; i >= 0; i--)        max_heapify(arr, i, len - 1);    //先將第一個元素和已排好元素前一位做交換,再從新調整,直到排序完畢    for (i = len - 1; i > 0; i--) {        swap(&arr[0], &arr[i]);        max_heapify(arr, 0, i - 1);    }}int main() {    int arr[] = { 3, 5, 3, 0, 8, 6, 1, 5, 8, 6, 2, 4, 9, 4, 7, 0, 1, 8, 9, 7, 3, 1, 2, 5, 9, 7, 4, 0, 2, 6 };    int len = (int) sizeof(arr) / sizeof(*arr);    heap_sort(arr, len);    int i;    for (i = 0; i < len; i++)        printf("%d ", arr[i]);    printf("\n");    return 0;}

1.最坏时间复杂度O(nlogn)
2.最优时间复杂度O(nlogn)
3.平均时间复杂度O(nlogn)
4.空间复杂度 O(n) total, O(1)auxiliary

计数排序

原理 使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数,然后根据数组c来将A中的元素排到正确的位置。
步骤
1.找出待排序的数组中最大和最小的元素
2.统计数组中每个值为i的元素出现的次数,存入数组 C 的第 i 项
3.对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
4.反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1

#include <stdio.h>#include <stdlib.h>#include <time.h>void print_arr(int *arr, int n) {    int i;    printf("%d", arr[0]);    for (i = 1; i < n; i++)        printf(" %d", arr[i]);    printf("\n");}void counting_sort(int *ini_arr, int *sorted_arr, int n) {    int *count_arr = (int *) malloc(sizeof(int) * 100);    int i, j, k;    for (k = 0; k < 100; k++)        count_arr[k] = 0;    for (i = 0; i < n; i++)        count_arr[ini_arr[i]]++;    for (k = 1; k < 100; k++)        count_arr[k] += count_arr[k - 1];    for (j = n; j > 0; j--)        sorted_arr[--count_arr[ini_arr[j - 1]]] = ini_arr[j - 1];    free(count_arr);}int main(int argc, char **argv) {    int n = 10;    int i;    int *arr = (int *) malloc(sizeof(int) * n);    int *sorted_arr = (int *) malloc(sizeof(int) * n);    srand(time(0));    for (i = 0; i < n; i++)        arr[i] = rand() % 100;    printf("ini_array: ");    print_arr(arr, n);    counting_sort(arr, sorted_arr, n);    printf("sorted_array: ");    print_arr(sorted_arr, n);    free(arr);    free(sorted_arr);    return 0;}

快速排序是二叉查找树(二叉查找树)的一个空间最优化版本。不是循序地把数据项插入到一个明确的
树中,而是由快速排序组织这些数据项到一个由递归调用所隐含的树中。这两个算法完全地产生相同的比较次数,但是顺序不同。对于排序算法的稳定性指标,原地分区版本的快速排序算法是不稳定的。其他变种是可以通过牺牲性能和空间来维护稳定性的。
快速排序的最直接竞争者是堆排序(Heapsort)。堆排序通常比快速排序稍微慢,但是最坏情况的运行时间总是O(n log n)。快速排序是经常比较快,除了introsort变化版本外,仍然有最坏情况性能的机会。如果事先知道堆排序将会是需要使用的,那么直接地使用堆排序比等待introsort再切换到它还要快。堆排序也拥有重要的特点,仅使用固定额外的空间(堆排序是原地排序),而即使是最佳的快速排序变化版本也需要Θ(log n)的空间。然而,堆排序需要有效率的随机存取才能变成可行。
快速排序也与归并排序(Mergesort)竞争,这是另外一种递归排序算法,但有坏情况O(n log n)运行时间的优势。不像快速排序或堆排序,归并排序是一个稳定排序,且可以轻易地被采用在链表(linked list)和存储在慢速访问媒体上像是磁盘存储或网络连接存储的非常巨大数列。尽管快速排序可以被重新改写使用在炼串列上,但是它通常会因为无法随机存取而导致差的基准选择。归并排序的主要缺点,是在最佳情况下需要Ω(n)额外的空间。

快速排序:https://zh.wikipedia.org/wiki/%E5%BF%AB%E9%80%9F%E6%8E%92%E5%BA%8F
冒泡排序:https://zh.wikipedia.org/wiki/%E5%86%92%E6%B3%A1%E6%8E%92%E5%BA%8F
选择排序:
http://www.cnblogs.com/QG-whz/p/5194569.html
http://blog.sina.com.cn/s/blog_eb52001d0102v1k8.html
https://zh.wikipedia.org/wiki/%E5%BD%92%E5%B9%B6%E6%8E%92%E5%BA%8F
http://www.cnblogs.com/kkun/archive/2011/11/23/2260275.html
https://zh.wikipedia.org/wiki/%E8%AE%A1%E6%95%B0%E6%8E%92%E5%BA%8F

原创粉丝点击