数据结构与算法之美(排序)

几种基本排序及其时间复杂度

在这里插入图片描述
几种最经典、最常用的排序方法 :冒泡排序、插入排序、选择排序、快速排序、归并排序、计数排序、基数排序、桶排序。

二、如何分析一个排序算法?

如何分析排序算法性能?从执行效率、内存消耗以及稳定性3个方面分析排序算法的性能。

1.执行效率(从以下3个方面来衡量)

1)最好情况、最坏情况、平均情况时间复杂度
2)时间复杂度的系数、常数、低阶:排序的数据量比较小时考虑
3)比较次数和交换(或移动)次数

2.内存消耗

算法的内存消耗可以通过空间复杂度来衡量,排序算法也不例外。不过,针对排序算法的空间复杂度,我们还引入了一个新的概念,原地排序(Sorted in place)。原地排序算法,就是特指空间复杂度是O(1)的排序算法。我们今天讲的三种排序算法,都是原地排序算法。

3.稳定性

仅仅用执行效率和内存消耗来衡量排序算法的好坏是不够的。针对排序算法,我们还有一个重要的度量指标,稳定性。这个概念是说,如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。

我通过一个例子来解释一下。比如我们有一组数据2,9,3,4,8,3,按照大小排序之后就是2,3,3,4,8,9。

这组数据里有两个3。经过某种排序算法排序之后,如果两个3的前后顺序没有改变,那我们就把这种排序算法叫作稳定的排序算法; 如果前后顺序发生变化,那对应的排序算法就叫作不稳定的排序算法

三、排序算法

1.冒泡排序

1)执行效率:最小时间复杂度、最大时间复杂度、平均时间复杂度

排序原理

1)冒泡排序只会操作相邻的两个数据。
2)对相邻两个数据进行比较,看是否满足大小关系要求,若不满足让它俩互换。
3)一次冒泡会让至少一个元素移动到它应该在的位置,重复n次,就完成了n个数据的排序工作。
4)优化:若某次冒泡不存在数据交换,则说明已经达到完全有序,所以终止冒泡。

性能分析

执行效率:最小时间复杂度、最大时间复杂度、平均时间复杂度
最小时间复杂度:数据完全有序时,只需进行一次冒泡操作即可,时间复杂度是O(n)。
最大时间复杂度:数据倒序排序时,需要n次冒泡操作,时间复杂度是O(n^2)。
平均时间复杂度:通过有序度和逆序度来分析。

扫描二维码关注公众号,回复: 14006488 查看本文章
什么是有序度?
有序度是数组中具有有序关系的元素对的个数
比如[2,4,3,1,5,6]这组数据的有序度就是11,分别是[2,4][2,3][2,5][2,6][4,5][4,6][3,5][3,6][1,5][1,6][5,6]。同理,对于一个倒序数组,比如[6,5,4,3,2,1],有序度是0;对于一个完全有序的数组,比如[1,2,3,4,5,6],有序度为n*(n-1)/2,也就是15,完全有序的情况称为满有序度。

什么是逆序度?逆序度的定义正好和有序度相反。核心公式:逆序度=满有序度-有序度。
排序过程,就是有序度增加,逆序度减少的过程,最后达到满有序度,就说明排序完成了。

2)空间复杂度

每次交换仅需1个临时变量,故空间复杂度为O(1),是原地排序算法。

3)算法稳定性

如果两个值相等,就不会交换位置,故是稳定排序算法。

2.插入排序

算法原理

首先,我们将数组中的数据分为2个区间,即已排序区间和未排序区间。初始已排序区间只有一个元素,就是数组的第一个元素。插入算法的核心思想就是取未排序区间中的元素,在已排序区间中找到合适的插入位置将其插入,并保证已排序区间中的元素一直有序。重复这个过程,直到未排序中元素为空,算法结束。

性能分析

1)时间复杂度:最好、最坏、平均情况

如果要排序的数组已经是有序的,我们并不需要搬移任何数据。只需要遍历一遍数组即可,所以时间复杂度是O(n)。

如果数组是倒序的,每次插入都相当于在数组的第一个位置插入新的数据,所以需要移动大量的数据,因此时间复杂度是O(n^2)。

而在一个数组中插入一个元素的平均时间复杂都是O(n),插入排序需要n次插入,所以平均时间复杂度是O(n^2)。

2)空间复杂度

从上面的代码可以看出,插入排序算法的运行并不需要额外的存储空间,所以空间复杂度是O(1),是原地排序算法。

3)算法稳定性

在插入排序中,对于值相同的元素,我们可以选择将后面出现的元素,插入到前面出现的元素的后面,这样就保持原有的顺序不变,所以是稳定的。

3.归并排序

待添加 … …

推荐题目(力扣)

912. 排序数组( 补充题4. 手撕快速排序)

https://leetcode-cn.com/problems/sort-an-array/

给你一个整数数组 nums,请你将该数组升序排列。

示例 1:

输入:
nums = [5,2,3,1]
输出:
[1,2,3,5]
示例 2:
输入:
nums = [5,1,1,2,0,0]
输出:
[0,0,1,1,2,5]

提示:

1 <= nums.length <= 5 * 104
-5 * 104 <= nums[i] <= 5 * 104

/*
只能说不讲武德  这个题目要优化快排,一开始有点莽  直接冲快排 然后不出意外的超时了  不死心的还去交了一下改进的代发   一样卡在第11 12哥测试答案上,然后就看了一下题解  题解说到了基准值改为随机函数取的一个值,然后去看了快排优化的几种思路;
关于快排 O(logn)  圈重点——————》不稳定  
第一种
    就是最原始的  取一个固定值当基准值 然后去对比 一般就是取第一个或者最后一个了  但是如果输入序列是随机的,处理时间可以接受的。如果数组已经有序时,此时的分割就是一个非常不好的分割。因为每次划分只能使待排序序列减一,此时为最坏情况,快速排序沦为冒泡排序了,时间复杂度为Θ(n^2)。而且,输入的数据是有序或部分有序的情况是相当常见的。
第二种
    就是这个这个题  用随机函数去取基准值  这样就不会造成刚刚所说的那个情况  除非整个数组的数据一模一样的 时间复杂度又会回到Θ(n^2);
    其实我感觉也没有优化什么  就只是避免了第一种所说的情况而已;
       (ps:亲测 我把随机函数改了  直接让中间的值来当基准值)
第三种
    三数取中法  这个可以弥补第二种的缺陷 但是它也比哪俩种都难写 这个不讲(因为我也太了解 hhhh 网上还有什么三路快排让我先去琢磨一下子)
*/
class Solution {
    
    
    int quicksort(vector<int>& nums, int l, int r) {
    
    
        int temp = nums[r];
        int i = l - 1;
        for (int j = l; j <= r - 1; ++j) {
    
    
            if (nums[j] <= temp) {
    
    
                i = i + 1;
                swap(nums[i], nums[j]);
            }
        }
        swap(nums[i + 1], nums[r]);
        return i + 1;
    }
    int partition(vector<int>& nums, int l, int r) {
    
    
        int i = (r+l)>>1; // 取中间的值当基准值
        swap(nums[r], nums[i]);
        return quicksort(nums, l, r);
    }
    void Sort(vector<int>& nums, int l, int r) {
    
    
        if (l < r) {
    
    
            int pos = partition(nums, l, r);
            Sort(nums, l, pos - 1);
            Sort(nums, pos + 1, r);
        }
    }
public:
    vector<int> sortArray(vector<int>& nums) {
    
    
        srand((unsigned)time(NULL));
       Sort(nums, 0, (int)nums.size() - 1);
        return nums;
    }
};
//快排必须要用随机函数  不然就会超时

82. 删除排序链表中的重复元素 II

给定一个已排序的链表的头 head , 删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表 。

示例 1:
在这里插入图片描述

输入:head = [1,2,3,3,4,4,5]
输出:[1,2,5]

示例 2:
在这里插入图片描述

输入:head = [1,1,1,2,3]
输出:[2,3]

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
    
    
public:
    ListNode* deleteDuplicates(ListNode* head) {
    
    
        if (head==NULL||head->next==NULL){
    
      //先判断是否为空
            return head;
        }
        if (head->val!=head->next->val){
    
     //如果当前值和下一个不一样,那么这个数据就是合法的,不需要删除,那么就将下一个递归,检查是否需要删除
            head->next=deleteDuplicates(head->next);
            return head;
        }
        
        else {
    
    
            int value=head->val;  //记录值
            while (head!=NULL&&head->val==value){
    
       //删除重复的所以值
                head=head->next;
            }
            if (head==NULL) // 如果是空,那么直接返回
                return NULL;
                head=deleteDuplicates(head);   //因为这个head就是下一个待检查的数据,所以不需要加next,一开始我加了,然后数据少了,有时候有多了  所以这个还是要注意一下,否则很容易出错
                return head;
        
        } 
    }
};

猜你喜欢

转载自blog.csdn.net/qq_54729417/article/details/123345047
今日推荐