算法整理(一)排序

冒泡排序:
void bubble_sort(vector<int> & nums) {
    int n = nums.size();
    for (int i = 0; i < nums.size(); ++i) {
        for (int j = n - 1; j > i; --j) {
            if (nums[j] < nums[j - 1]) {
                int temp = nums[j - 1];
                nums[j - 1] = nums[j];
                nums[j] = temp;
            }
        }
    }
    return;
}

算法复杂度:
1. 时间复杂度O(n^2),稳定,排序时间与输入无关,最好,最差,平均都是O(n^2)
2. 空间复杂度O(1)
3. 在排序过程中,执行完当前的第i趟排序后,可能数据已全部排序完备,但是程序无法判断是否完成排序,会继续执行剩下的(n-1-i)趟排序。解决方法: 设置一个flag位, 如果一趟无元素交换,则 flag = 0; 以后再也不进入第二层循环。优化的代码可以如下,这种情况就与输入有关了,复杂度可以为O(n)

void bubble_sort(vector<int> v){
    int end = v.size();
    while(end > 1){
        int new_end = 0;
        for(int j=0; j<end-1; j++){
            if(v[j] > v[j+1]){
                swap(v[j], v[j+1]);
                new_end = j+1;
            } 
        }
        end = new_end;
    }
}
插入排序

将数组后面未排序的数插入到前面已排好序的数组中。

void insert_sort(vector<int> & nums) {
    int n = nums.size(),temp = 0;
    for (int i = 1; i < n; ++i) {
        int temp = nums[i],j = i;
        for (; j >= 1 && nums[j - 1] > temp; --j) {
            nums[j] = nums[j - 1];
        }
        nums[j] = temp;
    }
    return;
}

两种优化方式:
1. 使用一个临时变量temp存储,不需要每次都三次交换。
2. 当找到一个合适的插入位置后就终止了内层循环。

算法复杂度:
1. 最佳情况,输入数组是已经排好序的数组,运行时间是n的线性函数; 最坏情况,输入数组是逆序,运行时间是n的二次函数。(内层循环为 1 + 2 +3 + 4 + … + n-1)平均时间复杂度:O(n^2)
2. 空间复杂度为:O(1)

选择排序

每次选取未排序部分最小的放在未排序部分的首位置。

void select_sort(vector<int> & nums) {
    int n = nums.size(),min = 0, temp = 0;
    for (int i = 0; i < n; ++i) {
        min = i;
        for (int j = i + 1; j < n; ++j) {
            if (nums[j] < nums[min]) {
                min = j;
            }
        }
        temp = nums[min];
        nums[min] = nums[i];
        nums[i] = temp;
    }
    return;
}

算法复杂度:
1. 时间复杂度O(n^2), 排序时间与输入无关,最佳情况,最坏情况都是如此
2. 空间复杂度O(1)

归并排序
void merge(vector<int> & nums, int low, int mid, int high) {
    vector <int> left(mid - low + 1), right(high - mid);
    //请注意,copy不负责分配内存!
    copy(nums.begin() + low, nums.begin() + mid + 1, left.begin());
    copy(nums.begin() + mid + 1, nums.begin() + high + 1, right.begin());
    int i = 0, j = 0;
    while (i < left.size() && j < right.size()) {
        if (left[i] < right[j]) {
            nums[low++] = left[i++];
        }
        else {
            nums[low++] = right[j++];
        }
    }
    while (i != left.size()) {
        nums[low++] = left[i++];
    }
    while(j != right.size()) {
        nums[low++] = right[j++];
    }
}
void divide_merge_sort(vector<int> & nums, int low, int high) {
    if (low < high) {
        int mid = (low + high) / 2;
        divide_merge_sort(nums, low, mid);
        divide_merge_sort(nums, mid + 1, high);
        merge(nums, low, mid, high);
    }
}
void merge_sort(vector<int> & nums) {
    int n = nums.size();
    divide_merge_sort(nums, 0, n - 1);
}

算法复杂度:
1. 归并排序的时间复杂度,合并耗费O(n)时间,而由完全二叉树的深度可知,整个归并排序需要进行log_2n次,因此,总的时间复杂度为 O(nlogn),而且这是归并排序算法中最好、最坏、平均的时间性能。
2. 由于归并排序在归并过程中需要与原始记录序列同样数量的存储空间存放归并结果 以及 递归时深度为 log_2n 的栈空间,因此空间复杂度为O(n+logn)
3. 也就是说,归并排序是一种比较占用内存,但却效率高且稳定的算法。

链表实现方式(摘录别人的),链表的实现上可以不用消耗额外的内存:

//链表结构单位
struct Node{
    int val;
    Node* next;
};

//node -- 链表表头
Node* MergeSort(Node* node){
    //先判断链表长度是否大于1,小于1时无须排序
    if(node!=NULL&&node->next!=NULL){
        //运用快慢指针,找到链表的中间节点
        Node *fast=node->next;
        Node *slow=node;
        while(fast!=NULL&&fast->next!=NULL){
            fast=fast->next->next;
            slow=slow->next;
        }

        //将链表分成两部分进行分割
        Node *p1=MergeSort(slow->next);
        slow->next=NULL;                 //这儿很重要,仔细想想为什么
        Node *p2=MergeSort(node);

        //对两条子链进行归并
        Node *p0=(Node *)malloc(sizeof(Node));
        Node *p=p0;
        while(p1!=NULL&&p2!=NULL){
            if(p1->val<p2->val){
                p->next=p1;
                p1=p1->next;
            }else{
                p->next=p2;
                p2=p2->next;
            }
            p=p->next;
        }

        if(p1!=NULL){
            p->next=p1;
        }

        if(p2!=NULL){
            p->next=p2;
        }

        p=p0->next;
        free(p0);
        return p;
    }

    return node;
}
快速排序
int partition(vector<int> & nums, int low, int high) {
    int pivot = low, last_small = low;
    for (int i = low + 1; i <= high; ++i) {
        if (nums[i] < nums[pivot]) {
            int temp = nums[i];
            nums[i] = nums[++last_small];
            nums[last_small] = temp;
        }
    }
    int temp = nums[pivot];
    nums[pivot] = nums[last_small];
    nums[last_small] = temp;
    return last_small;
}
void recursive_quick_sort(vector<int> & nums, int low, int high) {
    if (low < high) {
        int mid = partition(nums, low, high);
        recursive_quick_sort(nums, low, mid - 1);
        recursive_quick_sort(nums, mid + 1, high);
    }
}
void quick_sort(vector<int> & nums) {
    recursive_quick_sort(nums, 0, nums.size() - 1);
}

时间复杂度 O(nlogn) 空间复杂度O(logn) 不稳定 【两个时间复杂度O(nlogn) 的排序算法都不稳定】
时间复杂度:
最坏O(n^2) 当划分不均匀时候 逆序and排好序都是最坏情况
partition的时间复杂度: O(n)一共需要logn次partition
空间复杂度:递归造成的栈空间的使用,最好情况,递归树的深度logn 空间复杂的logn,最坏情况,需要进行n‐1 递归调用,其空间复杂度为 O(n),平均情况,空间复杂度也为O(log2n)。

希尔排序

把整个数组分为n组,每组m个元素(1A,2A,1B,2B)数字为组号,字母为组内元素。对组内元素进行排序(插入排序),然后不断放大组的大小,一直到整个数组:白话经典算法系列之三 希尔排序的实现

堆排序

最大堆和最小堆,实则都是二叉树。堆的性质:
1. 任意节点小于(或大于)它的所有后裔,最小元(或最大元)在堆的根上(堆序性)。
2. 堆总是一棵完全树。即除了最底层,其他层的节点都被元素填满,且最底层尽可能地从左到右填入。

堆排序的思想:
1. 给定一个数组,首先先把数组排成一个最小堆。从后往前,第一个非叶子节点开始(叶子节点肯定是满足的),让其成为一个最小堆,一直到数组下标为0的点则最小堆构造完成。
2.将最小堆的第一个元素(下标0)放于最后,再将最后一个元素放于下标0处重构最小堆(堆的大小要将最后一个元素排除在外,因为最后一个元素已经是最小了),重复直到排好序。

public class HeapSort {
    public void buildheap(int array[]){
        int length = array.length;
        int heapsize = length;
        int nonleaf = length / 2 - 1;
        for(int i = nonleaf; i>=0;i--){
            heapify(array,i,heapsize);
        }
    }

    public void heapify(int array[], int i,int heapsize){
        int smallest = i;
        int left = 2*i+1;
        int right = 2*i+2;
        if(left<heapsize){
            if(array[i]>array[left]){
                smallest = left;
            }
            else smallest = i;
        }
        if(right<heapsize){
            if(array[smallest]>array[right]){
                smallest = right;
            }
        }
        if(smallest != i){
            int temp;
            temp = array[i];
            array[i] = array[smallest];
            array[smallest] = temp;
            heapify(array,smallest,heapsize);
        }
    }

    public void heapsort(int array[]){
        int heapsize = array.length;
        buildheap(array);

        for(int i=0;i<array.length-1;i++){
            // swap the first and the last
            int temp;
            temp = array[0];
            array[0] = array[heapsize-1];
            array[heapsize-1] = temp;
            // heapify the array
            heapsize = heapsize - 1;
            heapify(array,0,heapsize);

        }   
    }

时间复杂度 O(nlogn), 空间复杂度O(1). 从这一点就可以看出,堆排序在时间上类似归并,但是它又是一种原地排序,空间复杂度小于归并的O(n+logn)
排序时间与输入无关,最好,最差,平均都是O(nlogn).

对于大数据的处理: 如果对100亿条数据选择Topk数据,选择快速排序好还是堆排序好? 答案是只能用堆排序。 堆排序只需要维护一个k大小的空间,即在内存开辟k大小的空间。而快速排序需要开辟能存储100亿条数据的空间,which is impossible.

计数排序
    public int[] countsort(int A[]){
        int[] B = new int[A.length]; //to store result after sorting
        int k = max(A);
        int [] C = new int[k+1]; // to store temp
        for(int i=0;i<A.length;i++){    
            C[A[i]] = C[A[i]] + 1;
        }
        // 小于等于A[i]的数的有多少个, 存入数组C
        for(int i=1;i<C.length;i++){
            C[i] = C[i] + C[i-1];
        }
        //逆序输出确保稳定-相同元素相对顺序不变
        for(int i=A.length-1;i>=0;i--){
            B[C[A[i]]-1] = A[i]; 
            C[A[i]] = C[A[i]]-1;
        }
        return B;
    }

最好,最坏,平均的时间复杂度O(n+k)

桶排序

思想:将所有元素放于n个桶中,桶内分别排序,则最后的结果就是排好序的。

基数排序

研究多个关键字的排序。有MSD和LSD(这是从次关键字开始的,更简单一点,但是每一次使用的排序是稳定的,即相等的话不影响原来的顺序)

时间复杂度


参考链接:常用排序算法总结(性能+代码)


稳定性问题

假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,ri=rj,且ri在rj之前,而在排序后的序列中,ri仍在rj之前,则称这种排序算法是稳定的;否则称为不稳定的。

堆排序、快速排序、希尔排序、直接选择排序不是稳定的排序算法,而基数排序、冒泡排序、直接插入排序、折半插入排序、归并排序是稳定的排序算法。

常见排序算法的稳定性分析和结论


STL中的sort算法

最优算法:

最优算法——若算法A在最坏情况(或平均情况)下是最优的,是指:算法A所在的算法类中的其他算法,在最坏(或平均)情况下,执行基本操作的次数不比A更少。
通常的做法是:
(1) 运用确定的基本操作,设计一个有较高效率的算法A,然后分析A,确定:对尺寸为n的任何输入,A至多做的基本操作次数W(n)。
(2) 证明一个定理,以此来说明对于所考虑的算法类中的任何一个算法,都存在一个尺寸为n的输入,使算法在最坏情况下,至少做F(n)次基本操作。
判断比较W(n)和F(n),若W(n)≤F(n),则称A是该算法类中一个在最坏情况下的最优算法,否则A不是最优算法

渐近最优:

归并排序和堆排序算法是渐进最优的,说到渐进这些算法是依赖于n的,并且忽略常数因子,这些算法随n的递增趋于最优。所以在基于比较的排序算法中,它们都是渐进最优的,它们只比较较少的次数,它们的运行时间主要受比较次数的影响,都是O(nlgn)。

STL中的sort函数使用的是快速排序。同时当递归深度大于2 * logn时会使用堆排序进行排序。当要排序的数组部分大小小于设定的阀值时,则使用插入排序算法。(快速排序如果两边分配不均衡,一边可能要进行堆排序,另一边就会执行插入排序)

个人理解:快排在平均情况下效率优于堆排序,不需要建树的过程。当递归深度过大时,栈空间会不足,效率也会下降,改用堆排序就能保持住效率问题。当数组大小小于阀值时,由于排序的数字数目不多,直接用插入排序效率也不会差。

STL sort 函数实现详解


算法比较

关于堆排序、归并排序、快速排序的比较,到底谁快

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值