《数据结构与算法之美》08——排序(一)冒泡排序、插入排序、选择排序

如何分析一个“排序算法”

从三个维度进行评价和分析:

1.排序算法的执行效率

  • 最好情况、最坏情况、平均情况时间复杂度
  • 时间复杂度的系统、常数、低阶
  • 比较次数和交换(或移动)次数

2.排序算法的内存消耗

  • 用空间复杂度来衡量。
  • 原地排序算法,特指空间复杂度是O(1)的排序算法。

3.排序算法的稳定性

  • 稳定的排序算法:相同元素的前后顺序没有改变的排序算法
  • 反之叫不稳定的排序算法。

冒泡排序

冒泡排序只会操作相邻的两个数据。每次冒泡操作都会对相邻的两个元素进行比较,如果不满足大小关系就互换。

/// <summary>
/// 冒泡排序,a表示数组,n表示数组大小
/// </summary>
/// <param name="a">数组</param>
/// <param name="n">数组大小</param>
public static void BubbleSort(int[] a, int n)
{
    if (n <= 1) return;
    for (int i = 0; i < n; ++i)
    {
        // 提前退出冒泡循环的标志位
        bool flag = false;
        for (int j = 0; j < n - i - 1; ++j)
        {
            if (a[j] > a[j + 1])
            { // 交换
                int tmp = a[j];
                a[j] = a[j + 1];
                a[j + 1] = tmp;
                flag = true; // 表示有数据交换
            }
        }
        if (!flag) break; // 没有数据交换,提前退出 
    }
}

分析排序算法的三个方面:

1.冒泡排序是原地排序算法吗?

冒泡的过程只涉及到相邻数据的交换,只需要常量级的临时空间,即空间复杂度为O(1),是原地排序算法。

2.冒泡排序是稳定的排序算法吗?

冒泡的过过,只交换不满足大小关系的元素,相等的两个元素不做交换,是稳定的排序算法。

3.冒泡排序的时间复杂度是多少?

  • 最好情况时间复杂度:O(n)。数据是有序的,只需要进行一次冒泡操作。
  • 最坏情况时间复杂度:O(n2)。数据是倒序的,要进行n次冒泡操作。
  • 平均情况时间复杂度:O(n2)

关于平均时间复杂度

如果用概率论方法定量分析平均时间复杂谎,涉及的数学推理和计算就会很复杂。下面介绍另外一种思路:通过“有序度”和“逆序度”两个概念进行分析。

  • 有序度:是数据中具有有序关系的元素对的个数。
  • 有序元素对:a[i] <= a[j],如果i < j。
  • 满有序度:完全有序的数组的有序度。
  • 逆序度:与有序度相反。
  • 逆序元素对:a[i] > a[j],如果i < j。
  • 以上概念得到一个公式:逆序度 = 满有序度 - 有序度

排序算法中,主要包含两个操作原子,比较和交换。不管算法怎么改进,交换次数总是确定的,即为逆序度,也就是n*(n-1)/2 - 初始有序度。

对于包含n个数据的数组进行冒泡排序,平均交换次数 = (最好情况 + 最坏情况)/2 = n*(n-1)/4。

平均情况下,比较操作肯定比交换操作要多,而复杂度上限是O(n2),所以平均情况时间复杂度是O(n^2)

插入排序

算法思路:往有序的数组里插入数据,找到插入位置,再移动后续的数据,即可完成。为此把数组分为两个区间,已排序区间和未排序区间。初始已排序区间只有一个元素,即数组的第一个元素,然后再遍历未排序区间(除第一个元素外的数组),把元素插入到已排序区间。

/// <summary>
/// 插入排序,a表示数组,n表示数组大小
/// </summary>
/// <param name="a">数组</param>
/// <param name="n">数组大小</param>
public static void InsertionSort(int[] a, int n)
{
    if (n <= 1) return;
    for (int i = 1; i < n; ++i)
    {
        int value = a[i];
        int j = i - 1;
        // 查找插入的位置
        for (; j >= 0; --j)
        {
            if (a[j] > value)
            {
                a[j + 1] = a[j]; // 数据移动
            }
            else
            {
                break;
            }
        }
        a[j + 1] = value; // 插入数据
    }
}

分析排序算法的三个方面:

1.插入排序是原地排序算法吗?

空间复杂度为O(1),是原地排序算法。

2.插入排序是稳定的排序算法吗?
插入时选择将后面出现的元素插入到前面出现元素的后面,这样原有前后顺序不变,是稳定的排序算法。

3.插入排序的时间复杂度是多少?

  • 最好情况时间复杂度:O(n)。数据是有序的,只需要遍历一次。
  • 最坏情况时间复杂度:O(n2)。数据是倒序的,要进行n次遍历和移动操作。
  • 平均情况时间复杂度:O(n2)。数组中插入一个数据的平均时间复杂度是O(n),循环执行n次,即平均时间复杂度为O(n2)

选择排序

选择排序算法的实现思路类似插入排序。分已排序区间和未排序区间,每次从未排序区间中找到最小的元素,将其放到已排序区间的末尾。

/// <summary>
/// 选择排序,a表示数组,n表示数组大小
/// </summary>
/// <param name="a">数组</param>
/// <param name="n">数组大小</param>
public void SelectionSort(int[] a, int n)
{
    if (n <= 1) return;
 
    for (int i = 0; i < n - 1; i++)
    {
        int value = a[i];
 
        // 找到第i个最小值
        for (int j = i + 1; j < n; j++)
        {
            if (value > a[j])
            {
                // 交换两值的位置
                int temp = value; 
                value = a[j];
                a[j] = temp;
            }
        }
 
        // 把第i个最小值放到第i位
        a[i] = value;
    }
}

分析排序算法的三个方面:

1.选择排序是原地排序算法吗?

空间复杂度为O(1),是原地排序算法。

2.选择排序是稳定的排序算法吗?

不是,选择交换的元素可能会破坏相等元素的原有顺序。

3.选择排序的时间复杂度是多少?

  • 最好情况时间复杂度:O(n2)。对于有序数组,仍然需要做n遍的遍历比较。
  • 最坏情况时间复杂度:O(n2)。对于逆序数组,需要做n遍的遍历比较和交换。
  • 平均情况时间复杂度:O(n2)。最好情况和最坏情况都是O(n2),平均也是O(n2)

为什么插入排序要比冒泡排序更受欢迎

冒泡排序不管怎么优化,元素交换的次数是一个固定值,是原始数据的逆序度。

插入排序不管怎么优化,元素移动的次数也等于原始数据的逆序度。

但在代码实现上,冒泡排序的数据交换要比插入排序的数据移动要复杂。冒泡排序需要3个赋值操作,而插入排序只需要1个。

冒泡排序中数据的交换操作:

if (a[j] > a[j+1]) { // 交换
    int tmp = a[j];
    a[j] = a[j+1];
    a[j+1] = tmp;
    flag = true;
}

插入排序中数据的移动操作:

if (a[j] > value) {
    a[j+1] = a[j]; // 数据移动
} else {
    break;
}

在大量数据的排序时,这一点点的差距就会放大,在性能要求高的环境,首先插入排序。

内容小结

冒泡排序 vs 插入排序 vs 选择排序

课后思考

我们讲过,特定算法是依赖特定的数据结构的。我们今天讲的几种排序算法,都是基于数组实现的。如果数据存储在链表中,这三种排序算法还能工作吗?如果能,那相应的时间、空间复杂度又是多少呢?
基于链表可以实现。

单链表之冒泡算法:

/// <summary>
/// 单链表冒泡排序
/// </summary> 
public static void BubbleSortByLinkedList(MyLinkedListNode head)
{
    if (head == null || head.Next == null || head.Next.Next == null) return;
 
    MyLinkedListNode curr = head.Next;
    while (curr != null)
    {
        // 提前退出冒泡循环的标志位
        bool flag = false;
 
        MyLinkedListNode node = curr.Next;
        while (node != null)
        {
            if (curr.Data > node.Data)
            {
                // 交换
                int temp = curr.Data;
                curr.Data = node.Data;
                node.Data = temp;
 
                flag = true; // 表示有数据交换
            }
 
            node = node.Next;
        }
 
        if (!flag) break; // 没有数据交换,提前退出 
 
        curr = curr.Next;
    }
}

时间复杂度:

  • 最好情况时间复杂度:O(n)
  • 最坏情况时间复杂度:O(n2)
  • 平均情况时间复杂度:O(n2)

空间复杂度:O(1)

单链表之插入排序:

/// <summary>
/// 单链表插入排序
/// </summary> 
public static void InsertionSortByLinkedList(MyLinkedListNode head)
{
    if (head == null || head.Next == null || head.Next.Next == null) return;
 
    MyLinkedListNode notSort = head.Next.Next;
    while (notSort != null)
    {
        MyLinkedListNode sorted = head.Next;
        MyLinkedListNode prevSorted = head;
 
        bool flag = false;
        while (sorted != null)
        {
            if (sorted.Data > notSort.Data)
            {
                var temp = notSort.Next;
                prevSorted.Next = notSort;
                notSort.Next = sorted;
                notSort = temp;
                flag = true;
                break;
            }
 
            prevSorted = sorted;
            sorted = sorted.Next;
        }
 
        if (!flag) notSort = notSort.Next;
    }
}

时间复杂度:

  • 最好情况时间复杂度:O(n2)
  • 最坏情况时间复杂度:O(n2)
  • 平均情况时间复杂度:O(n2)

空间复杂度:O(1)

单链表之选择排序:

/// <summary>
/// 单链表选择排序
/// </summary> 
public static void SelectionSortByLinkedList(MyLinkedListNode head)
{
    if (head == null || head.Next == null || head.Next.Next == null) return;
 
    MyLinkedListNode selected = head;
 
    while (selected != null)
    {
        MyLinkedListNode currNode, prevNode, minNode, prevMinNode;
        prevNode = prevMinNode = selected;
        currNode = minNode = selected.Next;
 
        bool flag = false;
 
        // 找到最小值结点和前置结点
        while (currNode != null)
        {
            if (currNode.Data < minNode.Data)
            {
                prevMinNode = prevNode;
                minNode = currNode;
                flag = true;
            }
 
            prevNode = currNode;
            currNode = currNode.Next;
        }
 
        if (flag)
        {
            MyLinkedListNode changedNode = selected.Next;  // 临时记录待交换结点,以便进行结点交换时进行赋值
            MyLinkedListNode minNextNode = minNode.Next; // 临时保存待交换最小结点的Next
 
            // 相邻两元素交换
            if (changedNode.Next == minNode)
            {
                selected.Next = minNode; // 已选择区间链接上最小结点
                minNode.Next = changedNode;
                changedNode.Next = minNextNode;
            }
            else
            {
                selected.Next = minNode; // 已选择区间链接上最小结点
                minNode.Next = changedNode.Next;
                prevMinNode.Next = changedNode;
                changedNode.Next = minNextNode;
            }
        }
 
        selected = selected.Next; 
    }
}

时间复杂度:

  • 最好情况时间复杂度:O(n2)
  • 最坏情况时间复杂度:O(n2)
  • 平均情况时间复杂度:O(n2)

空间复杂度:O(1)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,784评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,745评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,702评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,229评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,245评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,376评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,798评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,471评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,655评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,485评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,535评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,235评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,793评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,863评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,096评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,654评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,233评论 2 341