面试经典之------八大排序算法

1 篇文章 0 订阅

说实在的,刷了这么多题目,突然发现自己的基础是多么的差,今天看了一下八大排序算法,写起来还是费了不少功夫,八大排序算法包含7个基于比较的排序,和基于非比较的桶排序,接下来介绍一下经典的八大排序算法的原理及其代码实现。

冒泡排序:

冒泡排序是我们大多数人所接触的最早的排序算法,时间复杂度为O(N^2),空间复杂度为O(1),冒泡排序每次迭代将当前区间内的最大数沉底或者将最小数升顶,二者都为冒泡排序,我们就沉底法举例说明,假设当前有N个元素,第一次遍历之后,最大数会交换至最后一位上,第二次遍历发生在剩下的前N-1的位置上,以此类推,N次遍历完成排序,代码如下:

    void bubbleSort (vector<int>& nums) {
        int nlen = nums.size();
        if (nlen < 2) {
            return ;
        }
        for (int i=0; i<nlen; ++i) {
            for (int j=1; j<nlen-i; ++j) {
                if (nums[j] < nums[j-1]) {
                    swap (nums[j], nums[j-1]);
                }
            }
        }
    }

选择排序:

选择排序每次都是最小的值置于序列最前端,或者置于序列最后端,与冒泡排序所不同的是,冒泡排序每次都会有多个交换操作,而选择排序一次遍历仅需要一次操作,只需要找到当前区间内最大值的位置,而后于最后一个位置的数字交换即可。代码如下:

    void selectionSort (vector<int>& nums) {
        int nlen = nums.size();
        if (nlen < 2) {
            return ;
        }
        for (int i=0; i<nlen; ++i) {
            int idx = i;
            for (int j=i+1; j<nlen; ++j) {
                idx = nums[idx] < nums[j] ? idx:j;
            }
            swap (nums[idx], nums[i]);
        }
    }

插入排序:

插入排序是将有序序列逐渐扩大的过程,每一个数字i的前部序列已经有序,第i个数字依次与第(i-1)第(i-2)……元素进行对比,并与之交换位置,直到出现第(i)个元素比其前面某个元素大时,停止,从而使有序序列逐渐增大。具体代码实现如下:

    void insertSort (vector<int>& nums) {
        int nlen = nums.size();
        if (nlen < 2) {
            return ;
        }
        for (int i=1; i<nlen; ++i) {
            int j = i;
            while (j>0 && nums[j-1] > nums[j]) {
                swap (nums[j-1], nums[j]);
               --j;
           } 
        }
    }

以上三种方法均是时间复杂度为为:O(N^2), 空间复杂度为:O(1)。

接下来要介绍的是通过额外空间减低时间复杂度的排序算法:

快速排序:

快速排序的基本与原理是每次选择一个数,将区间内大于该数的所有数值放在该数值的右侧,小于该数值的数放在左侧,每次交换结束之后,选择的数就放置在了排序序列的正确位置上,因为左边都是小于它的数,右边都是大于它的数。而后利用递归,对左右两部分在进行相同的操作,每次层操作的时间复杂度是O(N),最好情况要进行O(lgN)次递归,所以,时间复杂度是O(NlgN),空间复杂度是O(lgN)。代码如下:

    void quickSort (vector<int>& nums) {
        int nlen = nums.size();
        if (nlen < 2) {
            return ;
        }
        quickSort (nums, 0, nlen-1);
    }
    void quickSort (vector<int>& nums, int left, int right) {
        if (left >= right) {
            return ;
        }
        int idx = left + rand()%(right-left+1);
        swap (nums[idx], nums[right]);
        int start = left;
        for (int i=left; i<right; ++i) {
            if (nums[i] >= nums[right]) {
                continue;
            }
            swap (nums[i], nums[start++]);
        }
        swap (nums[start], nums[right]);
        quickSort (nums, left, start-1);
        quickSort (nums, start+1, right);
    }

归并排序:

归并排序和快速排序区别在于,一个是先分再排,一个是先排再分,归并排序的空间复杂度是O(N),时间复杂度稳定,都是O(NlgN),具体的方法是将数列分为左右两部分,将左右两部分排好序,再将整个数列排好序,而每一部分的排序方法与上述方法类似,这种情况很利于用递归实现。具体的代码如下:

    void mergeSort (vector<int>& nums) {
        int nlen = nums.size();
        if (nlen < 2) {
            return ;
        }
        mergeSort (nums, 0, nlen-1);
    }
    void mergeSort (vector<int>& nums, int left, int right) {
        if (left >= right) {
            return ;
        }
        int mid = left+(right-left)/2;
        mergeSort (nums, left, mid);
        mergeSort (nums, mid+1, right);
        vector<int> tmp;
        int i = left, j = mid+1;
        while (i<=mid && j<=right) {
            tmp.push_back(nums[i] < nums[j] ? nums[i++] : nums[j++]);
        }
        while (i<=mid) {
            tmp.push_back(nums[i++]);
        }
        while (j<=right) {
            tmp.push_back(nums[j++]);
        }
        for (int ans : tmp) {
            nums[left++] = ans;
        }
    }

堆排序:

说起堆排序,我一直以为需要构建二叉树,因为每次学习原理的时候都是用二叉树来表示,后来发现,其实只是利用二叉树的形,实则是利用数组下标的连接关系来实现,堆排序分为两个过程,1、构建堆。 2、调整堆。

构建堆时,利用先上后下,从下往上的原则进行构建,新元素不断加入,并搜索当前节点与其父节点之间的大小关系,及时调整,因元素个数共有N个,每次调整的时间复杂度是O(lgN),所以总的时间复杂度是O(NlgN)。

    void heapSort (vector<int>& nums) {
        int nlen = nums.size();
        if (nlen < 2) {
            return ;
        }
        for (int i=0; i<nlen; ++i) {
            heapInsert (nums, i);
        }
        swap (nums[0], nums[nlen-1]);
        int j = nlen-2;
        while (j) {
            heapAjust(nums, 0, j);
            swap (nums[j], nums[0]);
            --j;
        }
    }
    void heapInsert (vector<int>& nums, int idx) {
        while (idx && nums[(idx-1)/2] < nums[idx]) {
            swap (nums[idx], nums[(idx-1)/2]);
            idx = (idx-1)/2;
        }
    }
    void heapAjust (vector<int>& nums, int curidx, int lens) {
        int child = 2*curidx+1;
        while (child <= lens) {
            int largest = child+1<=lens && nums[child+1]>nums[child] ? child+1 : child;
            largest = nums[largest] > nums[curidx] ? largest : curidx;
            if (largest == curidx) {
                break;
            }
            swap (nums[largest], nums[curidx]);
            curidx = largest;
            child = 2*curidx+1;
        }
    }

希尔排序:

希尔排序平时用的不多,其实是插入排序的一种优化方式,其时间复杂度取决于步长的选择,插入排序的步长始终是1,而希尔排序利用多个不同的步长分别进行插入排序,依次减少数值交换发生次数,在部分场景中表现优异,具体代码如下:

    void shellSort (vector<int>& nums) {
        int nlen = nums.size();
        if (nlen < 2) {
            return ;
        }
        int step = nums.size()/2;
        for (int i=step; i>0; --i) {
            shellSort (nums, i, nlen);
        }
    }
    void shellSort (vector<int>& nums, int step, int nlen) {
        for (int i=step; i<nlen; ++i) {
            int left = i-step, right = i;
            while (left >= 0 && nums[left]>nums[right]) {
                swap (nums[left], nums[right]);
                right = left;
                left = left-step;
            }
        }
    }

以上就是所有基于比较的排序算法。

接下来基于非比较排序的算法就是桶排序。实际上是将一定范围内的每个数字对应到一个桶中,然后按照顺序从桶中倒出,举个例子,1,6,7,3,9, 我们根据数值设定九个桶,而后从一号桶开始倒出,一号桶倒出1,二号桶为空,三号倒出3,四号桶空……循环往复下去,相应的桶排序实现方法有两种。

计数排序:

计数排序适合数据密集型的场景,如果数据稀疏,就会出现大面积的空间浪费现象,他是将每个数据进行计数,即统计每个数字出现的次数,而后进行累加,这时,每个桶对应的数值就是该数在排序后的编号,也就是应该倒出的顺序。举例:

1,0,1 这个数列,对应的0号桶数值为1, 1号桶的数值为3,也就是说,经过排序之后,3-1的位置上应该是数值1,第二次遇到数值0, 零号桶的数值为1,排序后0应该在位置0上,每次结束之后,桶值减一,再次遇到1时,1号桶的值为2,则位置1上因该是1.具体代码实现如下:

    void countingSort (vector<int>& nums) {
        int nlen = nums.size();
        if (nlen < 2) {
            return;
        }
        int mx = nums[0], mn = nums[0];
        for (int ans : nums) {
            mx = max (mx, ans);
            mn = min (mn, ans);
        }
        
        vector<int> buckets(mx-mn+1, 0);
        for (int ans : nums) {
            ++buckets[ans-mn];
        }
        for (int i=0; i<mx-mn; ++i) {
            buckets[i+1] += buckets[i];
        }
        vector<int> tmp (nlen, 0);
        for (int ans : nums) {
            tmp[--buckets[ans-mn]] = ans;
        }
        nums = tmp;
    }

基数排序:

上面提到基数排序在针对数据比较分散的情况下,无法很好的工作,会严重浪费存储,因此,针对计数排序存在的问题,基数排序分别对个十百千万每个位进行排序,最终达到排列整个数列的目的,因为是每个位进行排序,所以,桶的个数固定10个即可,值得注意的是,基数排序的单次迭代都是使用的是计数排序,而且,为了不影响当前位的排序情况,保持当前排序结果,从后往前遍历是由严格规定的。代码实现如下:

    void radixSort (vector<int>& nums) {
        int nlen = nums.size();
        if (nlen < 2) {
            return ;
        }
        int mx = nums[0];
        for (int ans : nums) {
            mx = max (mx, ans);
        }
        int d = 0;
        while (mx) {
            ++d;
            mx /= 10;
        }
        int mods = 1;
        while (d) {
            vector<int> buckets(10, 0);
            for (int ans : nums) {
                ++buckets[(ans/mods)%10];
            }
            for (int i=0; i<9; ++i) {
                buckets[i+1] += buckets[i];
            }

            vector<int> tmp (nlen, 0);
            for (int i = nlen-1; i>=0; --i) {
                tmp[--buckets[(nums[i]/mods)%10]] = nums[i];
            }
            nums = tmp;
            --d;
            mods *= 10;
        }
    }

以上为经典排序算法的简单原理及代码实现,原理部分如果不清楚,看看代码其实就很好理解了。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值