数据结构(六):冒泡排序、选择排序、插入排序、快速排序

数据结构(六)

  • 一、大O表示法
  • 二、冒泡排序
  • 三、选择排序
  • 四、插入排序
  • 五、快速排序

一、大O表示法

在计算机中采用粗略的度量来描述计算机算法的效率,这种方法被称为“大O”表示法。
我们判断一个算法的效率,不能只凭着算法运行的速度,因为随着数据量的变化,算法的速度会发生变化,所以我们应该:

根据算法的速度随着数据量的变化会如何变化,这样的方式来表示算法的效率,大O表示法就是方式之一。

推导大O表示法:
规则一:用常量1取代运行时间中所有的加法常量。如7 + 8 = 15,用1表示运算结果15,大O表示法表示为O(1);
规则二:运算中只保留最高阶项。如N^3 + 3n +1,大O表示法表示为:O(N³);
规则三:若最高阶项的常数不为1,可将其省略。如4N2,大O表示法表示为:O(N²);

接下来是我们的集中排序算法:
简单排序:冒泡排序、选择排序、插入排序;
高级排序:希尔排序、快速排序;
我们封装一个列表来存储数据和排序算法

class ArrayList {
	 constructor() {
	     this.arr = []
	 }
	
	 insert(element) {
	     return this.arr.push(element);
	 }
	
	 toString() {
	     return this.arr.join(' ');
	 }
}
	
	let list = new ArrayList();
	list.insert(4);
	list.insert(5);
	list.insert(2);
	list.insert(1);
	list.insert(3);
	console.log(list.toString());

二、冒泡排序

数据结构(六):冒泡排序、选择排序、插入排序、快速排序_第1张图片

我先自己写了一遍,我发现我写的这个其实是有问题的,内层循环控制两个元素依次比较,外层循环控制比较的趟数。这样写虽然能实现,但是你会发现其实内层循环每次都要比较arr.length-1次,而实际上后面元素如果排好的话,根本不需要再比较了,比如21345,那么345就不用再比较了。

1.冒泡排序
bubbleSort() {
   for(let i = 0; i < this.arr.length-1; i++) {
       for(let i = 0; i < this.arr.length-1; i++) {
	       if(this.arr[i] > this.arr[i+1]) {
	       		//交换两个位置的值
	           let zzy = this.arr[i+1];
	           this.arr[i+1] = this.arr[i];
	           this.arr[i] = zzy;
	       }
   		}
   }
   return this.arr
}

这样的话就需要进行一些小小的改进:
改进的就是这个for循环的次数,拿[4,2,1,3]来举例,外层循环控制趟数,那么4个数比较3趟,依次递减(j=3第一趟,j=2第二趟,j=1第三趟),每一趟中都要两两比较,从下标为0开始,依次比较j次(j=3第一趟比较3次,j=2第二趟比较2次,j=1第三趟比较1次)。

总结:4个数要比较三趟,第一趟比较3次,第二趟比较2次,第三趟比较1次

bubbleSort() {
   for (var j = this.arr.length - 1; j > 0; j--) {
       for (var i = 0; i < j; i++) {
           if (this.arr[i] > this.arr[i + 1]) {
               let zzy = this.arr[i + 1];
               this.arr[i + 1] = this.arr[i];
               this.arr[i] = zzy;
           }
       }
   }
   return this.arr
}

数据结构(六):冒泡排序、选择排序、插入排序、快速排序_第2张图片

冒泡排序的效率:

上面所讲的对于7个数据项,比较次数为:6 + 5 + 4 + 3 + 2 + 1;
对于N个数据项,比较次数为:(N - 1) + (N - 2) + (N - 3) + … + 1 = N * (N - 1) / 2;
如果两次比较交换一次,那么交换次数为:N * (N - 1) / 4;

使用大O表示法表示比较次数和交换次数分别为:O(N*(N - 1)/2)和O(N*(N - 1)/4),根据大O表示法的三条规则都化简为:O(N²);

三、选择排序

选择排序的思路:从所有元素中找到最小的一个,和第一个元素交换位置;从后面元素中找到最小的一个,和第二个元素交换位置……,直到length-1最后一个位置停止。

selectionSort() {
    for (var j = 0; j < this.arr.length - 1; j++) {
        let minIndex = j;
        for (var i = minIndex + 1; i < this.arr.length; i++) {
            if (this.arr[i] < this.arr[minIndex]) {
                minIndex = i;
            }
        }
        //交换两个位置元素
        let temp = this.arr[minIndex];
        this.arr[minIndex] = this.arr[j];
        this.arr[j] = temp;
    }
    return this.arr;
}

还是拿[4,3,1,2]举例吧,代码思路就是默认0位置第一个数是最小的,然后内层循环从索引1位置开始比较,找到最小数1索引2后,和索引04交换([1,3,4,2]);然后外层循环j=1,下一层内层循环应该从索引2处的4开始比较,找到索引3处的2,和索引1处的3交换[1,2,4,3]);然后外层循环j=2,以此类推,直到外层循环走到倒数第二个位置。

选择排序的效率:
选择排序的比较次数为:N * (N - 1) / 2,用大O表示法表示为:O(N²);
选择排序每一趟交换一次,交换次数为:(N - 1) / 2,用大O表示法表示为:O(N);
所以选择排序的效率高于冒泡排序;

四、插入排序

插入排序的思路:默认第一个数是有序的,然后前两个数变成有序,然后前三个数变成有序……也就是说第1趟比较前2个,第二趟比较前3个……如果比较时最后一个直接就是最大的,那么就break跳出循环。
[4,3,1,2,5]举例,我画了个图
从上到下分别是每一趟,从左到右分别是每一次比较

数据结构(六):冒泡排序、选择排序、插入排序、快速排序_第3张图片

//3.插入排序
insertionSort() {
    //外层循环,从第一个位置开始获取数据。
    for (let j = 1; j < this.arr.length; j++) {
        //内层循环,从当前位置依次向前比较
        for (let i = j - 1; i >= 0; i--) {
            if (this.arr[i] > this.arr[j]) {
                //交换位置
                let temp = this.arr[j]; //保存当前数据
                this.arr[j] = this.arr[i];
                this.arr[i] = temp;
                j--; //j往前挪
            } else {
                break; //如果比前边的数大,就退出循环
            }
        }
    }
    return this.arr;
}

插入排序的效率:
比较次数:第一趟最多比较1次;第二趟最多比较2次;以此类推,最后一趟最大为N-1;所以,插入排序的总比较次数为N * (N - 1) / 2;但是,实际上每趟发现插入点之前,平均只有全体数据项的一半需要进行比较,所以比较次数为:N * (N - 1) / 4;

交换次数也是同理:指一共需要交换N * (N - 1) / 2次,平均次数为N * (N - 1) / 4;

虽然用大O表示法表示插入排序的效率也是O(N²),但是插入排序整体操作次数更少(除非[5,4,3,2,1],不然不用比较和交换那么多次就排完了),因此,在简单排序中,插入排序效率最高;

五、快速排序

1、先选取枢纽,这里选择第一个元素为枢纽(其实选择首尾和中间位置元素三者的中位数比较好,方法就是先把三个数取出来冒泡排序,然后找到中位数作为枢纽pivot)

2、定义左右两个指针,依次和枢纽比较,如果左边比枢纽大,左指针停,否则右移;如果右边比枢纽小,右指针停,否则向左移动;然后左右指针位置的数进行交换,直到左右指针相撞,那么所指位置的数就和枢纽交换。本次排序就完成了。

3、接下来递归调用,分别对枢纽左侧和枢纽右侧重复执行上述操作,直到满足某个条件开始出栈,最后返回arr

4、递归停止的条件:begin == end 此时说明枢纽左侧或右侧只有一个元素,不需要排序;还有begin > end 此时说明枢纽在第一个元素(begin=1,end=-1),枢纽左侧没东西了不需要递归了,左侧就出栈

function quickSort(arr, begin = 0, end = arr.length - 1) {
    if(begin >= end) return; //递归开始出栈的条件
    let i = begin; //定义一个左指针,指向第一个元素
    let j = end; //定义一个右指针,指向最后一个元素
    let pivot = arr[i]; //把枢纽设置为第一个元素
    //当左右指针重合(i==j)时,交换当前指针元素和枢纽位置元素
    while(i < j) {
        //只要没有重合,就继续迭代指针
        //先看右指针,如果右指针指向的数比pivot大,说明是正确的,继续向左查找
        while(i < j && arr[j] >= pivot) {
            //上面条件中,i < j 是为了防止指针迭代时i超过j,这是不对的
            j--;
        }
        //再看左指针,如果左指针指向的数比pivot小,说明是正确的,继续向右查找
        while(i < j && arr[i] <= pivot) {
            i++;
        }
        //左右指针都停了,就交换两个指针的数
        [arr[i],arr[j]] = [arr[j], arr[i]];
    }
    //指针碰撞,交换枢纽(我们设置的是begin位置)和当前位置
    [arr[begin], arr[i]] = [arr[i], arr[begin]];
   	//,第一次排序结束,接下来左右分别递归
    quickSort(arr, begin, i - 1); //左边递归,确定起始结束位置
    quickSort(arr, i + 1, end); //右边递归,确定起始结束位置
}

你可能感兴趣的:(数据结构与算法,数据结构,排序算法,算法,javascript)