目录
什么是稳定算法,什么是不稳定算法?
稳定算法:如果遇到相等的值不进行交换,那这种排序方式是稳定的排序方式。
不稳定算法,改变相同元素之间顺序,即1,2,10,3,10。对于10这个元素,他们原有的相对位置可能会发生变化
算法复杂度分为时间复杂度和空间复杂度
时间复杂度:
在计算机科学中,时间复杂性,又称时间复杂度,算法的时间复杂度是一个函数,它定性描述该算法的运行时间。这是一个代表算法输入值的字符串的长度的函数。时间复杂度常用大O符号表述,不包括这个函数的低阶项和首项系数。使用这种方式时,时间复杂度可被称为是渐近的,亦即考察输入值大小趋近无穷时的情况。
摘自百度百科
代表的是一种趋势,一个函数表达式中,趋势和最高阶项有关,所以
用最高阶项代替。
复杂度分析法则:
单段代码看高频,比如,for循环,取最高循环次数
var a=1; //执行1次
for(var i=0;i<n,i++){ //执行n+1次
var b=2; //执行n次
var c=4; //执行n次
}
//上述复杂度为1+n+1+n+n=3n+2,故时间复杂度为O(n)
多段代码取最大:比如多个for循环代码片段,取最高阶for循环项
var a=1; //执行1次
for(var i=0;i<n,i++){ //执行n+1次
var b=2; //执行n次
var c=4; //执行n次
}
var a=1; //执行1次
for(var i=0;i<n*n,i++){ //执行n*n+1次
var b=2; //执行n*n次
}
//上述复杂度为1+n+1+n+n +1+n*n+1+n*n=2*n*n+3n+3,故时间复杂度为O(n*n)
嵌套代码取最大,利用乘法原则,比如多层for循环,取每层for循环最高循环次数乘积最高次项目,还有递归调用
var a=1; //执行1次
for(var i=0;i<n,i++){ //执行n+1次
var b=2; //执行n次
var c=4; //执行n次
for(var i=0;i<n*n,i++){ //执行n(n*n+1)次
var b=2; //执行n*n*n次
}
}
//上述复杂度为1+n+1+n+n +1+n3+n+n3=2*n3+4n+3,故时间复杂度为O(n3)
多个规模求加法,利用加法原则,比如方法有两个参数控制两个循环的次数,那么就是两者相加.
Tips:常量执行次数的时间复杂度为O(1)
复杂度分类:
1、最坏情况时间复杂度:代码在最坏的情况执行的时间复杂度
for(var i=1;i<n;i++){
if(i<m) break;
}
//最坏的情况是if条件始终不成立,代码执行n次,故时间复杂度为O(n)
//最好的情况是if条件第一次就成立,代码执行m次,m为常数,故时间复杂度为O(1)
2、最好情况时间复杂度:代码在最理想的情况下执行的次数
3、平均时间复杂度:代码在所有情况下执行的次数的加权平均值
4、均摊时间复杂度:在代码执行的所有复杂度情况中绝大部分是低级别的复杂度,个别情况是高级别复杂度且发生具有时序关系时,可以将个别高级别复杂度均摊到低级别复杂度上。基本上均摊结果就等于低级别复杂度。
为什么要引入这引入这4个概念?
代码在不同情况下,执行的时间复杂度会有差异,为了更全面准确的描述代码的时间复杂度,所以引入这4个概念,只有代码执行出现量级别差别时,才需要区分这四种复杂度,绝大部分算法不需要区分他们。
数据结构:
定义:一组数据的存储结构
算法
操作数据的一组方法。
数据结构是为算法服务的,算法是要作用在特定的数据结构上
数据结构分类:
线性结构:数组、链表、栈、队列
非线性结构:散列表、二叉树,堆、图、树
二叉树
数据结构(二)之二叉树 - dreamcatcher-cx - 博客园
数据结构(一)之线性表 - dreamcatcher-cx - 博客园
排序算法方法分类:
交换:通过交换两个元素的位置来进行排序
插入:把元素插入已经排序好的队列中
选择:每次选择最大或最小元素,插入副本队列元素,直到所有元素选择完毕。
简单排序算法(选择)
第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。选择排序是不稳定的排序方法。
在算法实现时,每一趟确定最小元素的时候会通过不断地比较交换来使得首位置为当前最小,交换是个比较耗时的操作。其实我们很容易发现,在还未完全确定当前最小元素之前,这些交换都是无意义的。我们可以通过设置一个变量min,每一次比较仅存储较小元素的数组下标,当轮循环结束之后,那这个变量存储的就是当前最小元素的下标,此时再执行交换操作即可。代码实现很简单,一起来看下。
/**
* 简单选择排序
*
* @param arr
*/
public static void selectSort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
//每一趟循环比较时,min用于存放较小元素的数组下标,
//这样当前批次比较完毕最终存放的就是此趟内最小的元素的下标,
//避免每次遇到较小元素都要进行交换。
int min = i;
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[min]) {
min = j;
}
}
//进行交换,如果min发生变化,则进行交换
if (min != i) {
swap(arr,min,i);
}
}
}
冒泡排序
基本原理
它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果顺序(如从大到小、首字母从Z到A)错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素列已经排序完成。
实现:
1、使用两个嵌套的循环,其中,外层循环移动游标,内层循环遍历游标以及之后(或之前)的元素。
2、通过两两交换的方式,每次只确保该内层循环结束位置排序正确。
3、一次内层循环结束,会确定当前循环最大或最小元素位置。
4、内层循环结束后,交由外层循环向前或者向后移动游标。随即开始下一轮循环。
5、以此类推,直至没有元素进行比较结束。
由于冒泡排序有两层for循环,可以有四种实现方式
方案 | 外层循环 | 内层循环 |
---|---|---|
1 | 正序 | 正序 |
2 | 正序 | 逆序 |
3 | 逆序 | 正序 |
4 | 逆序 | 逆序 |
四种实现方式略有差异
基本元素交换函数
function swap(i,j,arr){
var temp=arr[j];
arr[j]=arr[i];
arr[i]=temp;
}
正序正序
function bubbleSort(array) {
var length = array.length, isSwap;
for (var i = 0; i < length; i++) { //正序
isSwap = false;
for (var j = 0; j < length - 1 - i; j++) { //正序
array[j] > array[j+1] && (isSwap = true) && swap(j,j+1,array);
}
if(!isSwap)
break;
}
return array;
}
逆序逆序
function bubbleSort(array) {
var length = array.length, isSwap;
for (var i = length - 1; i >= 0; i--) { //逆序
isSwap = false;
for (var j = length - 1; j >= length - 1 - i; j--) { //逆序
array[j] < array[j-1] && (isSwap = true) && swap(j,j-1,array);
}
if(!isSwap)
break;
}
return array;
}
正序逆序
function bubbleSort(array) {
var length = array.length, isSwap;
for (var i = 0; i < length; i++) { //正序
isSwap = false;
for (var j = length - 1; j >= i+1; j--) { //逆序
array[j] < array[j-1] && (isSwap = true) && swap(j,j-1,array);
}
if(!isSwap)
break;
}
return array;
}
逆序正序
function bubbleSort(array) {
var length = array.length, isSwap;
for (var i = length; i > 0; i--) { //逆序
isSwap = false;
for (var j = 0; j <= i-1; j++) { //正序
array[j] < array[j+1] && (isSwap = true) && swap(j,j+1,array);
}
if(!isSwap)
break;
}
return array;
}
双向冒泡排序
1、先让气泡从左往右进行,再让气泡从右往左进行,如此完成一次排序的动作
2、使用left与right来记录左右已排序的元素的位置
3、当left>=right时,则排序完成。
function bothwayBubbleSort(array){
var tail = array.length-1, i, isSwap = false;
for(i = 0; i < tail; tail--){
for(var j = tail; j > i; j--){ //第一轮, 先将最小的数据冒泡到前面
array[j-1] > array[j] && (isSwap = true) && swap(j,j-1,array);
}
i++;
for(j = i; j < tail; j++){ //第二轮, 将最大的数据冒泡到后面
array[j] > array[j+1] && (isSwap = true) && swap(j,j+1,array);
}
}
return array;
}
tips:两头冒泡,左边最小,右边最大,最后排序位置左侧和右侧相等,则排序完成
插入排序
工作原理:
在待排序的元素中,假设前面n-1(其中n>=2)个数已经是排好顺序的,现将第n个数插到前面已经排好的序列中,然后找到合适自己的位置,使得插入第n个数的这个序列也是排好顺序的。按照此法对所有元素进行插入,直到整个序列排为有序的过程,称为插入排序
插入排序的工作方式像许多人排序一手扑克牌。开始时,我们的左手为空并且桌子上的牌面向下。然后,我们每次从桌子上拿走一张牌并将它插入左手中正确的位置。为了找到一张牌的正确位置,我们从右到左将它与已在手中的每张牌进行比较。拿在左手上的牌总是排序好的,原来这些牌是桌子上牌堆中顶部的牌
插入排序由于插入方式的不同,又可以分为直接插入排序,折半插入排序、希尔排序、链表插入排序
直接插入排序
工作原理
将一条记录插入到已排好的有序表中,从而得到一个新的、记录数量增1的有序表,直到插完所有的元素为止
function directInsertionSort(array) {
var length = array.length, index, current;
for (var i = 1; i < length; i++) {
index = i - 1;
//待比较元素的下标
current = array[i];
//当前元素
console.group("start" + i);
console.group("startwhile");
while (index >= 0 && array[index] > current) {
//前置条件之一:待比较元素比当前元素大
array[index + 1] = array[index];
//将待比较元素后移一位
index--;
//游标前移一位
console.log(array);
}
console.groupEnd();
console.group("startif")
if (index + 1 != i) {
//避免同一个元素赋值给自身
array[index + 1] = current;
//将当前元素插入预留空位
console.log(array);
}
console.groupEnd();
console.groupEnd()
}
return array;
}
可见每次for循环,当前元素之前的元素都是有序的。并且不会改变相同元素之间的排序,是稳定算法。
折半插入排序
工作原理
折半插入排序(binary insertion sort)是对插入排序算法的一种改进,由于排序算法过程中,就是不断的依次将元素插入前面已排好序的序列中。由于前半部分为已排好序的数列,这样我们不用按顺序依次寻找插入点,可以采用折半查找的方法来加快寻找插入点的速度。
function binaryInsertionSort(array) {
var current, i, j, low, high, m;
for (i = 1; i < array.length; i++) {
low = 0;
high = i - 1;
current = array[i];
console.group("start" + i)
console.group("startWhile")
while (low <= high) {
//步骤1&2:折半查找
m = Math.floor(low + high);
if (array[i] >= array[m]) {//值相同时, 切换到高半区,保证稳定性
low = m + 1; //插入点在高半区
} else {
high = m - 1; //插入点在低半区
}
console.log("low" + low);
console.log("high" + high);
}
console.groupEnd();
console.group("startfor")
for (j = i; j > low; j--) {
//步骤3:插入位置之后的元素全部后移一位
array[j] = array[j - 1];
console.log(array)
}
console.groupEnd();
array[low] = current;//步骤4:插入该元素
console.log(array);
console.groupEnd();
}
return array;
}
明细折半插入排序减少了查询次数
希尔排序
把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至 1 时,整个文件恰被分成一组,算法便终止。
1、排序长度的折半进行分组,对分组进行排序后,对所有分组元素左侧元素进行组合,右侧元素进行组合形成新的分组。
2、对步骤1形成新的数组再进行步骤1步长的一半再进行分组排序
//形参增加步数gap(实际上就相当于gap替换了原来的数字1)
function directInsertionSort(array, gap) {
gap = (gap == undefined) ? 1 : gap; //默认从下标为1的元素开始遍历
var length = array.length, index, current;
for (var i = gap; i < length; i++) {
index = i - gap; //待比较元素的下标
current = array[i]; //当前元素
while(index >= 0 && array[index] > current) { //前置条件之一:待比较元素比当前元素大
array[index + gap] = array[index]; //将待比较元素后移gap位
index -= gap; //游标前移gap位
}
if(index + gap != i){ //避免同一个元素赋值给自身
array[index + gap] = current; //将当前元素插入预留空位
}
}
return array;
}
function shellSort(array){
var length = array.length, gap = length>>1, current, i, j;
while(gap > 0){
directInsertionSort(array, gap); //按指定步长进行直接插入排序
gap = gap>>1;
}
return array;
}
归并排序
基本原理:
归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
function mergeSort(array) { //采用自上而下的递归方法
var length = array.length;
if(length < 2) {
return array;
}
var m = (length >> 1),
left = array.slice(0, m),
right = array.slice(m); //拆分为两个子数组
return merge(mergeSort(left), mergeSort(right));//子数组继续递归拆分,然后再合并
}
function merge(left, right){ //合并两个子数组
var result = [];
while (left.length && right.length) {
var item = left[0] <= right[0] ? left.shift() : right.shift();//注意:判断的条件是小于或等于,如果只是小于,那么排序将不稳定.
result.push(item);
}
return result.concat(left.length ? left : right);
}
快速排序
设要排序的数组是A[0]……A[N-1],首先任意选取一个数据(通常选
用数组的第一个数)作为关键数据,然后将所有比它小的数都放到它左边,所有比它大的数都放到它右边,这个过程称为一趟快速排序。值得注意的是,快速排序不是一种稳定的排序算法,也就是说,多个相同的值的相对位置也许会在算法结束时产生变动。 [1]
一趟快速排序的算法是: [1]
1)设置两个变量i、j,排序开始的时候:i=0,j=N-1; [1]
2)以第一个数组元素作为关键数据,赋值给key,即key=A[0]; [1]
3)从j开始向前搜索,即由后开始向前搜索(j--),找到第一个小于key的值A[j],将A[j]和A[i]的值交换; [1]
4)从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的A[i],将A[i]和A[j]的值交换; [1]
5)重复第3、4步,直到i==j; (3,4步中,没找到符合条件的值,即3中A[j]不小于key,4中A[i]不大于key的时候改变j、i的值,使得j=j-1,i=i+1,直至找到为止。找到符合条件的值,进行交换的时候i, j指针位置不变。另外,i==j这一过程一定正好是i+或j-完成的时候,此时令循环结束)。
function quickSort(array, left, right) {
var partitionIndex,
left = typeof left == 'number' ? left : 0,
right = typeof right == 'number' ? right : array.length-1;
if (left < right) {
partitionIndex = partition(array, left, right);//切分的基准值
quickSort(array, left, partitionIndex-1);
quickSort(array, partitionIndex+1, right);
}
return array;
}
function partition(array, left ,right) { //分区操作
for (var i = left+1, j = left; i <= right; i++) {//j是较小值存储位置的游标
array[i] < array[left] && swap(i, ++j, array);//以第一个元素为基准
}
swap(left, j, array); //将第一个元素移至中间
return j;
}
快速排序排序效率非常高. 虽然它运行最糟糕时将达到O(n²)的时间复杂度, 但通常, 平均来看, 它的时间复杂为O(nlogn), 比同样为O(nlogn)时间复杂度的归并排序还要快. 快速排序似乎更偏爱乱序的数列, 越是乱序的数列, 它相比其他排序而言, 相对效率更高.
堆排序
将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了
堆
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。如下图:
算法的基本思想(以大根堆为例):
- 先将初始序列K[1..n]建成一个大根堆, 此堆为初始的无序区.
- 再将关键字最大的记录K1 (即堆顶)和无序区的最后一个记录K[n]交换, 由此得到新的无序区K[1..n-1]和有序区K[n], 且满足K[1..n-1].keys≤K[n].key
- 交换K1 和 K[n] 后, 堆顶可能违反堆性质, 因此需将K[1..n-1]调整为堆. 然后重复步骤2, 直到无序区只有一个元素时停止.
function heapAdjust(array, i, length) {//堆调整
var left = 2 * i + 1,
right = 2 * i + 2,
largest = i;
if (left < length && array[largest] < array[left]) {
largest = left;
}
if (right < length && array[largest] < array[right]) {
largest = right;
}
if (largest != i) {
swap(i, largest, array);
heapAdjust(array, largest, length);
}
}
function heapSort(array) {
//建立大顶堆
length = array.length;
for (var i = length>>1; i >= 0; i--) {
heapAdjust(array, i, length);
}
//调换第一个与最后一个元素,重新调整为大顶堆
for (var i = length - 1; i > 0; i--) {
swap(0, i, array);
heapAdjust(array, 0, --length);
}
return array;
}
计数排序
对于给定的输入序列中的每一个元素x,确定该序列中值小于x的元素的个数(此处并非比较各元素的大小,而是通过对元素值的计数和计数值的累加来确定)。一旦有了这个信息,就可以将x直接存放到最终的输出序列的正确位置上。例如,如果输入序列中只有17个元素的值小于x的值,则x可以直接存放在输出序列的第18个位置上。当然,如果有多个元素具有相同的值时,我们不能将这些元素放在输出序列的同一个位置上,因此,上述方案还要作适当的修改。
计数排序利用了一个特性, 对于数组的某个元素, 一旦知道了有多少个其它元素比它小(假设为m个), 那么就可以确定出该元素的正确位置(第m+1位)
function countSort(array, keyName){
var length = array.length,
output = new Array(length),
max,
min,
simpleArray = keyName ? array.map(function(v){
return v[keyName];
}) : array; // 如果keyName是存在的,那么就创建一个只有keyValue的简单数组
// 获取最大最小值
max = min = simpleArray[0];
simpleArray.forEach(function(v){
v > max && (max = v);
v < min && (min = v);
});
// 获取计数数组的长度
var k = max - min + 1;
// 新建并初始化计数数组
var countArray = new Array(k);
simpleArray.forEach(function(v){
countArray[v - min]= (countArray[v - min] || 0) + 1;
});
// 累加计数,存储不同值的初始下标
countArray.reduce(function(prev, current, i, arr){
arr[i] = prev;
return prev + current;
}, 0);
// 从原数组挨个取值(因取的是原数组的相应值,只能通过遍历原数组来实现)
simpleArray.forEach(function(v, i){
var j = countArray[v - min]++;
output[j] = array[i];
});
return output;
}
桶排序
将数组分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)
桶排序的核心就在于怎么把元素平均分配到每个桶里, 合理的分配将大大提高排序的效率.
function bucketSort(array, bucketSize) {
if (array.length === 0) {
return array;
}
var i = 1,
min = array[0],
max = min;
while (i++ < array.length) {
if (array[i] < min) {
min = array[i]; //输入数据的最小值
} else if (array[i] > max) {
max = array[i]; //输入数据的最大值
}
}
//桶的初始化
bucketSize = bucketSize || 5; //设置桶的默认大小为5
var bucketCount = Math.floor((max - min) / bucketSize) + 1, //桶的个数
buckets = new Array(bucketCount); //创建桶
for (i = 0; i < buckets.length; i++) {
buckets[i] = []; //初始化桶
}
//将数据分配到各个桶中,这里直接按照数据值的分布来分配,一定范围内均匀分布的数据效率最为高效
for (i = 0; i < array.length; i++) {
buckets[~~((array[i] - min) / bucketSize)].push(array[i]);
}
array.length = 0;
for (i = 0; i < buckets.length; i++) {
quickSort(buckets[i]); //对每个桶进行排序,这里使用了快速排序
for (var j = 0; j < buckets[i].length; j++) {
array.push(buckets[i][j]); //将已排序的数据写回数组中
}
}
return array;
}
桶也只是一个抽象的概念, 它的思想与归并排序,快速排序等类似, 都是通过将大量数据分配到N个不同的容器中, 分别排序, 最后再合并数据. 这种方式大大减少了排序时整体的遍历次数, 提高了算法效率.
基数排序
将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
第一步
以LSD为例,假设原来有一串数值如下所示:
73, 22, 93, 43, 55, 14, 28, 65, 39, 81
首先根据个位数的数值,在走访数值时将它们分配至编号0到9的桶子中:
0
1 81
2 22
3 73 93 43
4 14
5 55 65
6
7
8 28
9 39
第二步
接下来将这些桶子中的数值重新串接起来,成为以下的数列:
81, 22, 73, 93, 43, 14, 55, 65, 28, 39
接着再进行一次分配,这次是根据十位数来分配:
0
1 14
2 22 28
3 39
4 43
5 55
6 65
7 73
8 81
9 93
第三步
接下来将这些桶子中的数值重新串接起来,成为以下的数列:
14, 22, 28, 39, 43, 55, 65, 73, 81, 93
Tips:~~就是Math.floor
function radixSort(array, max) {
var buckets = [],
unit = 10,
base = 1;
for (var i = 0; i < max; i++, base *= 10, unit *= 10) {
for(var j = 0; j < array.length; j++) {
var index = ~~((array[j] % unit) / base);//依次过滤出个位,十位等等数字
if(buckets[index] == null) {
buckets[index] = []; //初始化桶
}
buckets[index].push(array[j]);//往不同桶里添加数据
}
var pos = 0,
value;
for(var j = 0, length = buckets.length; j < length; j++) {
if(buckets[j] != null) {
while ((value = buckets[j].shift()) != null) {
array[pos++] = value; //将不同桶里数据挨个捞出来,为下一轮高位排序做准备,由于靠近桶底的元素排名靠前,因此从桶底先捞
}
}
}
}
return array;
}
这时候整个数列已经排序完毕;如果排序的对象有三位数以上,则持续进行以上的动作直至最高位数为止。
LSD的基数排序适用于位数小的数列,如果位数多的话,使用MSD的效率会比较好。MSD的方式与LSD相反,是由高位数为基底开始进行分配,但在分配之后并不马上合并回一个数组中,而是在每个“桶子”中建立“子桶”,将每个桶子中的数值按照下一数位的值分配到“子桶”中。在进行完最低位数的分配后再合并回单一的数组中。
三数取中法
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列