1.排序的定义:
输入:n个记录的序列为{r1,r2,……,rn},其相应的关键字分别为{k1,k2……,kn}
输出:确定1,2,……,n的一种排列p1,p2,……,pn,使其相应的关键字满足k(p1)<=k(p2)<=k(p3)<=...<=k(pn)。即使得序列称为一个按照关键字有序的序列{r(p1),r(p2),……,r(pn)}。
也就是对n个序列对象按照某个关键字进行排序。
2.评价算法优劣术语的说明:
稳定:假设ki=kj(i,j介于1~n,且不相等),在排序前的序列中ri在rj前面(即i<j)。排序后ri仍在rj前面。
不稳定:排序后ri在rj后面。
内排序:在排序的整个过程中,待排序的所有记录全部被放置在内存中;
外排序:由于排序的记录个数太多,不能同时放置在内存中,整个排序过程需要在内外存之间多次交换数据才能进行。
时间复杂度: 一个算法执行所耗费的时间。
空间复杂度: 运行完一个程序所需内存的大小。
3.算法详述 ;
(1)冒泡排序(Bubble Sort):
冒泡排序是一种交换排序。其基本思想两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止。由于第i次循环都是将i~n个记录中的最小的关键字经过交换顺序浮到第i个位置,故名“冒泡”。
实现代码如下:
//冒泡排序算法
function bubbleSort(arr){
for(var i=0; i<arr.length-1; i++){
for(var j=arr.length-1; j>i; j--){
if(arr[j]<arr[j-1]){
var temp = arr[j];
arr[j] = arr[j-1];
arr[j-1] = temp;
}
}
}
return arr;
}
算法的改进:设置一个标记变量flag来标记每趟循环中是否进行了数据的交换。若本次循环未进行数据交换,则代表后续的数据均是有序的,不用再进行后续的循环 比较了。
//冒泡排序算法的改进
function bubbleSort(arr){
var flag = true;
for(var i=0; i<arr.length-1 && flag; i++){
flag = false;
for(var j=arr.length-1; j>i; j--){
if(arr[j]<arr[j-1]){
var temp = arr[j];
arr[j] = arr[j-1];
arr[j-1] = temp;
flag = true; //如果有数据交换,则flag为true
}
}
}
return arr;
}
(2)简单选择排序(Simple Selection Sort):
简单选择排序算法即为通过n-i次关键字间的比较,从n-i+1个记录中选出关键字最小的记录,并和第i(1<=i<=n)个记录交换。
实现代码如下:
//选择排序算法
function selectSort(arr){
for(var i=0; i<arr.length-1; i++){
var min = i;
for(var j=i+1; j<arr.length; j++){
if(arr[j] < arr[min]){
min = j;
}
}
var temp = arr[i];
arr[i] = arr[min];
arr[min] = temp;
}
return arr;
}
(3)直接插入排序(Straight Insertion Sort):
直接插入排序的思想是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数加1的有序表。在排序的过程中需要反复的将排好序的数据逐步后移,为新元素找到合适的位置。
实现代码如下:
//直接插入排序算法
function insertSort(arr){
for(var i=1; i<arr.length; i++){
var key = arr[i];
for(j=i-1; j>=0; j--){
if(arr[j]>key){
arr[j+1] = arr[j];
arr[j] = key;
}
}
}
return arr
}
改进算法:查找插入位置时使用二分查找法
function binaryInsertSort(arr) {
var len =arr.length;
for (var i=1;i<len; i++) {
var key=arr[i],left=0,right=i-1;
while(left<=right){ //在已排序的元素中二分查找第一个比它大的值
var mid= parseInt((left+right)/2); //二分查找的中间值
if(key<arr[mid]){ //当前值比中间值小 则在左边的子数组中继续寻找
right = mid-1;
}else{
left=mid+1;//当前值比中间值大 在右边的子数组继续寻找
}
}
for(var j=i-1;j>=left;j--){
arr[j+1]=arr[j];
}
arr[left]=key;
}
return arr;
}
(4)希尔排序(Shill Sort):
希尔排序是第一个突破O(n^2)的排序算法;是简单插入排序的改进版;它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
- <1>. 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
- <2>.按增量序列个数k,对序列进行k 趟排序;
- <3>.每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
实现代码如下:
//希尔排序算法
function shellSort(arr){
var N = arr.length;
var h = 1;
while(h < N/3){
h = 3*h+1; //定义间隔序列
}
while(h >= 1){
for(var i = h; i < N; i++){
for(j = i; j >= h && arr[j] < arr[j-h]; j-=h){
var temp = arr[j];
arr[j] = arr[j-h];
arr[j-h] = temp;
}
}
h = (h-1) / 3;
}
return arr;
}
(5)堆排序(Heap Sort):
堆排序的思想是将待排序的序列构造成一个大顶堆,此时整个序列的最大值即为堆顶的根结点。将其移走后,将剩余的n-1个序列重新构造成一个堆,这样就会得到n个元素中的次小值。如此反复执行,即可得到一个有序序列。
/*方法说明:堆排序
@param array 待排序数组*/
function heapSort(array) {
console.time('堆排序耗时');
if (Object.prototype.toString.call(array).slice(8, -1) === 'Array') {
//建堆
var heapSize = array.length, temp;
for (var i = Math.floor(heapSize / 2) - 1; i >= 0; i--) {
heapify(array, i, heapSize);
}
//堆排序
for (var j = heapSize - 1; j >= 1; j--) {
temp = array[0];
array[0] = array[j];
array[j] = temp;
heapify(array, 0, --heapSize);
}
console.timeEnd('堆排序耗时');
return array;
} else {
return 'array is not an Array!';
}
}
/*方法说明:维护堆的性质
@param arr 数组
@param x 数组下标
@param len 堆大小*/
function heapify(arr, x, len) {
if (Object.prototype.toString.call(arr).slice(8, -1) === 'Array' && typeof x === 'number') {
var l = 2 * x + 1, r = 2 * x + 2, largest = x, temp;
if (l < len && arr[l] > arr[largest]) {
largest = l;
}
if (r < len && arr[r] > arr[largest]) {
largest = r;
}
if (largest != x) {
temp = arr[x];
arr[x] = arr[largest];
arr[largest] = temp;
heapify(arr, largest, len);
}
} else {
return 'arr is not an Array or x is not a number!';
}
}
(6)归并排序(Mergiing Sort):
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。归并排序是一种稳定的排序方法。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
归并排序算法完全依照分治模式,直观的操作如下:
- 分解:将n个元素分成各含n/2个元素的子序列;
- 解决:用归并排序法对两个子序列递归地排序;
- 合并:合并两个已排序的子序列以得到排序结果。
实现代码如下:
//归并排序算法
function mergeSort(arr){
if(arr.length <= 1){
return arr;
}
var middle = Math.floor(arr.length / 2),
//slice()方法从已有数组返回start开始,end(不包含)结束的元素,返回截取的新数组
left = arr.slice(0, middle),
right = arr.slice(middle);
return merge(mergeSort(left), mergeSort(right));
}
function merge(left, right){
var result = [];
while(left.length && right.length){
if(left[0] <= right[0]){
//shift()方法用于把数组的第一个元素从其中删除,并返回第一个元素的值
result.push(left.shift());
}else{
result.push(right.shift());
}
}
while(left.length){
result.push(left.shift());
}
while(right.length){
result.push(right.shift());
}
return result;
}
(7)快速排序(Quick Sort)
快速排序的原理基于冒泡排序改进的,其思想是:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小。则可分别对着两部分记录继续进行排序,以达到整个序列有序的目的。
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists):
- 从数列中挑出一个元素,称为 "基准"(pivot);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
设要排序的数组是A[0]....A[N-1],首先任意选取一个数据(通常选取用数组的第一个数)作为关键数据key,然后比key小的数据都放到key的前面,比key大的数据都放在key的后面,这个过程称为一趟快速排序。
其算法思路如下:
- 设置两个变量i,j,排序开始的时候,i=low;j = high;(第一趟low=0,high为数组长度N-1)
- 以第一个数组元素为关键元素,赋值给key,即pivot= A[0];
- 从j开始像前搜索,找到第一个比pivot小的数a[j],将a[j]和a[i]交换。
- 从i开始向后搜素,找到第一个比pivot大的数a[i],将a[i]和a[j]交换。
- 重复3,4步直到i==j
- 对pivot左边和右边分别递归调用以上步骤。
实现代码如下:
//快速排序算法
function quickSort(arr){
if(arr.length <= 1){
return arr;
}
var left = [];
var right =[];
var pivotkey = arr[0];
for(var i=1; i<arr.length; i++){
if(arr[i] < pivotkey){
left.push(arr[i]);
}else{
right.push(arr[i]);
}
}
return quickSort(left).concat(pivotkey, quickSort(right));
}
4.快速排序与归并排序的区别:
- 快速排序的时间复杂度是O (nlogn),但是快速排序是一种不稳定的排序方法,也就是说当有多个相同的值的时候在排序结束的时候它们的相对位置会发生改变。
- 归并排序的时间复杂度是O (nlogn),但是归并排序是一种稳定的排序方法,即相等的元素顺序不会改变,但是相比于快速排序来说归并要申请的空间更大,消耗空间更多。
5.各个算法的性能比较如下图所示: