数据结构与算法-学习笔记(七)

318 阅读4分钟

排序

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

  1. 排序算法的执行效率
  • 最好、最坏、平均情况时间复杂度:为甚要区分这三种时间复杂度?第一,有些排序算法会区分。第二,对于要排序的数据,有的接近有序、有的完全无序。有序度不同的数据,对于排序的执行时间肯定有影响,我们要知道排序算法在不同数据下的性能表现。
  • 时间复杂度的系数、常数、低阶:因为实际排序中数据规模可能比较小10个、100个这样的,所以把系数、常数、低阶也考虑进来更加准确。
  • 比较次数和交换(或移动)次数:如果是基于比较的排序算法。执行过程中涉及两种操作:1.元素比较大小;2.交换或移动。因此,在分析排序算法的执行效率时还要考虑次数。
  1. 排序算法的内存消耗
  • 空间复杂度
  • 新概念:原地排序(特指空间复杂度为O(1))的排序算法。
  1. 排序算法的稳定性:指如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。那么为什么要稳定性呢?因为真实排序是对真实对象可能通过某个属性来排序,这时可能同时要求其他属性也是有序的,如下订单按金额排序,相同金额按时间排序:
    这时保持原本顺序不变就变得重要了。

具体排序算法

冒泡排序

思路:比较和交换。每次把当前未排序的数据中最大的数据找出,放在最后。往水里扔个大石头,石头下沉,比石头轻的气泡上升。

function bubbleSort(array) {
    var length = array.length;
    for (let j = length; j > 1; j--) {
        let exchange = false;
        for (let i = 0; i < j-1; i++) {
            if (array[i] > array[i+1]) {
                let temp = array[i];
                array[i] = array[i+1];
                array[i+1] = temp;
                exchange = true;
            }
        }
        if (!exchange) return;
        
     }
}

对于平均复杂度,粗略的计算:

冒泡排序包含两个操作:比较和交换。交换次数最好情况=0;最差情况=n*(n-1)/2,平均交换次数粗略来看是n*(n-1)/4,比较次数肯定比交换次数多,因此在加上比较次数,所以平均复杂度大致是O(n2)。

插入排序

思路:比较和插入

将数组分为两个区域:已排序区域和未排序区域。主要操作已排序区域,最初就是数组第一个元素时有序,其余无序;每次从无序区域拿一个元素和前面有序区域元素比较,再插入合适位置。

function insertSort(array) {
    for (let i = 1; i < array.length; i++) {
        let currentValue = array[i];
        let j = i-1;
        for (; j >= 0 ; j--) {
            if (array[j] > currentValue) {
                array[j+1] = array[j];
            } else {
                break;
            }
        }
        array[j+1] = currentValue;
    }
}

最好的时间复杂度是完全有序:O(n)

最坏的时间复杂度完全逆序:O(n2)

平均时间复杂度:向数组插入一个元素的平均复杂度是O(n),而插入排序算法每个元素都需要插入一次,因此是O(n2)

选择排序

思路:查找和交换。同插入类似,分出已排序和未排序区间。但是选择排序主要操作未排序区域,每次从未排序区间找出最小元素,将其放在已排序区间的末尾。

function selectSort(array) {
    for (let i = 0; i < array.length; i++) {
        let currentMinnum = array[i];
        let currentMinnumIndex = i;
        for (let j = i + 1; j < array.length; j++) {
            if (currentMinnum > array[j]) {
                currentMinnum = array[j];
                currentMinnumIndex = j;
            } 
        }
        let temp = array[currentMinnumIndex];
        array[currentMinnumIndex] = array[i];
        array[i] = temp;  
    }
}

可以看出无论哪种数据排序,时间复杂度都是一样的O(n2);

以上三种排序比较

冒泡和插入排序时间复杂度、空间复杂度、稳定性都是一样的,但是为什么说插入要比冒泡更好呢?主要是冒泡每次循环执行交换操作是3步,而插入仅是一步移动。因此当数据量比较大时,且希望把性能优化到极致的话,首选插入排序。

如果数据结果变为链表呢?

是否允许修改链表的节点value值,还是只能改变节点的位置。一般而言,考虑只能改变节点位置,冒泡排序相比于数组实现,比较次数一致,但交换时操作更复杂;插入排序,比较次数一致,不需要再有后移操作,找到位置后可以直接插入,但排序完毕后可能需要倒置链表;选择排序比较次数一致,交换操作同样比较麻烦。综上,时间复杂度和空间复杂度并无明显变化,若追求极致性能,冒泡排序的时间复杂度系数会变大,插入排序系数会减小,选择排序无明显变化。