最近想系统地看看数据结构和算法,虽然实际工作中用到的不多(也许是水平不够不知道要用...),但还是要多培养这方面的意识和思想,再加上大厂面试基本都要考察数据结构和算法,所以还是要把这块捡起来。当然,还是先从排序走起。上一次系统地看排序还是在大学里,刚接触编程不久,当时还是用C和Java写的,如今做了前端,C和Java几年不看也丢得差不多了,只能写写JS了。
1.冒泡排序
实现原理
依次比较相邻的两个元素,如果第一个元素大于第二个元素就交换它们的位置。这样比较一轮之后,最大的元素就会跑到队尾。然后对未排序的序列重复这个过程,最终转换成有序序列。
代码实现
function bubble(arr){
//经过一次循环后最大的元素会被放在数组尾端
for(let i = 0;i < arr.length;i++){
//最后一个元素位置已固定,只需遍历1~n-1个元素
for(let j = 0;j < arr.length - i - 1;j++){
if(arr[j] > arr[j+1]){
//如果数组本身是顺序的,执行0次;如果数组本身是逆序的,执行n*(n-1)/2次
let t = arr[j];
arr[j] = arr[j+1];
arr[j+1] = t;
}
}
}
return arr
}
算法分析
判断语句if(arr[j] > arr[j+1])需要执行1+2+...+n-1次,而且与序列的初始状态(是否有序)无关,但是判断里面的语句和序列的初始状态有关,所以时间复杂度为O(n2),空间复杂度为O(1)
2.插入排序
实现原理
认为第一个元素是排好序的,从第二个开始遍历。
拿出当前元素的值,从排好序的序列中从后往前找。
如果序列中的元素比当前元素大,就把它后移。直到找到一个小的。
把当前元素放在这个小的后面(后面的比当前大,它已经被后移了)。
代码实现
function insert(arr){
//从第二个元素开始遍历
for(var i = 1;i < arr.length;i++){
var t = arr[i];//保存待插入元素
//从后往前遍历“有序”部分
for(var j = i-1;j >= 0 ;j--){
if(arr[j] > t){//如果待插入元素小,那么将有序序列中的元素后移
arr[j+1] = arr[j];
}else{//如果待插入元素更大,则执行插入操作,插入结束后退出内层循环
arr[j+1] = t;
break
}
//如果有序序列已经遍历完,则执行插入操作
if(j == 0){
arr[j] = t;
}
}
}
return arr
}
更简洁一点的写法:
function insert(arr) {
var len = arr.length;
var preIndex, current;
for (var i = 1; i < len; i++) {
preIndex = i - 1;
current = arr[i];
//这里用一个while循环,把arr[preIndex] > current这个判断写到条件里面,就不用像上面一样啰嗦
while(preIndex >= 0 && arr[preIndex] > current) {
arr[preIndex+1] = arr[preIndex];
preIndex--;
}
arr[preIndex+1] = current;
}
return arr
}
算法分析
最坏的情况(初始序列逆序):第一次插入比较1次,第二次插入比较2次,第n-1次插入比较n-1次,所以时间复杂度为O(n2),空间复杂度为O(1)。
3.选择排序
实现原理
首先从未排序序列中找到最小的元素,放置到排序序列的起始位置,然后从剩余的未排序序列中继续寻找最小元素,放置到已排序序列的末尾,所以称之为选择排序。
代码实现
function pick(arr){
//初始时,已排序序列为空,未排序序列为arr,已排序序列在前
for(var i = 0;i < arr.length;i++){
//找出未排序序列中最小的元素
var min = arr[i],index = i;
for(var j = i+1;j < arr.length;j++){
//这个判断执行了1+2+...+n-1次,时间复杂度为O(n2)
if(arr[j] < min){
//找到未排序序列中的最小元素
min = arr[j];
index = j;//记录下标
}
}
//把未排序序列中最小的元素和已排序序列的末尾元素互换(很重要)
arr[index] = arr[i]
arr[i] = min;
}
return arr
}
算法分析
同样,比较要进行1+2+3+...+n-1次,时间复杂度为O(n2),空间复杂度为O(1)。
4.快速排序
实现原理
在数据集之中,选择一个元素作为”基准”(pivot)。所有小于”基准”的元素,都移到”基准”的左边;所有大于”基准”的元素,都移到”基准”的右边。这个操作称为分区 (partition)。操作,分区操作结束后,基准元素所处的位置就是最终排序后它的位置。
对”基准”左边和右边的两个子集,不断重复第一步和第二步,直到所有子集只剩下一个元素为止。
代码实现
//说明:想要通过“夹逼法”确定一个元素的位置,需要两个指针
function partition(arr, start, end){
var pivot = arr[start]
while(start < end){
if(arr[end] > pivot){
end--;
}else{
arr[start] = arr[end];
start++;
arr[end] = arr[start]
}
}
arr[start] = pivot
return start;
}
function sort(array, lo, hi) {
if (lo >= hi) {
return array;
}
var index = partition(array, lo, hi);
sort(array, lo, index - 1);
sort(array, index + 1, hi);
return array;
}
算法分析
快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。
5.希尔排序
实现原理
先取一个正整数 d1(d1 < n),把全部记录分成 d1 个组,所有距离为 d1 的倍数的记录看成一组,然后在各组内进行插入排序。然后取 d2(d2 < d1)重复上述分组和排序操作;直到取 di = 1(i >= 1) 位置,即所有记录成为一个组,最后对这个组进行插入排序。
代码实现
function shellSort(arr) {
var len = arr.length,
temp,
k = 1;//定义步长
while(k < len/3) { //动态定义间隔序列
k = k*3 + 1;
}
for (k; k > 0; k = Math.floor(k/3)) {
for (var i = k; i < len; i++) {
temp = arr[i];
for (var j = i-k; j >= 0 && arr[j] > temp; j-=k) {
arr[j+k] = arr[j];
}
arr[j+k] = temp;
}
}
return arr;
}
算法分析
希尔排序是插入排序的一种更高效的改进版本,针对插入排序的两个特性做出改进:1.插入排序在对几乎已经排好序的数据操作时效率较高;2.插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。另外,步长的选择对于算法的效率影响也较大,代码中的取法k = k*3 + 1;也是经过考验的效率较高的一种,更多关于步长的研究请看维基。平均时间复杂度是O(n log2 n),空间复杂度是O(1)。
6.归并排序
实现原理
把 n 个记录看成 n 个长度为 l 的有序子表进行两两归并使记录关键字有序,得到 n/2 个长度为 2 的有序子表。重复第 2 步直到所有记录归并成一个长度为 n 的有序表为止。
总而言之,归并排序就是使用递归,先分解数组为子数组,再合并数组。
代码实现
function mergeSort(arr) { // 采用自上而下的递归方法
var len = arr.length;
if(len < 2) {
return arr;
}
var middle = Math.floor(len / 2),
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]) {
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;
}
算法分析
归并排序的性能不受输入数据的影响,始终都是 O(nlogn) 的时间复杂度。代价是需要额外的内存空间,所以空间复杂度为O(n)。
7.堆排序
实现原理
堆排序的原理很简单,只要熟悉堆这个数据结构就很容易理解。堆排序就是把最大堆堆顶的最大数取出,将剩余的堆继续调整为最大堆,再次将堆顶的最大数取出,这个过程持续到剩余数只有一个时结束。
代码实现
这个代码自己没有实现,下面是github上找来的一段代码:
var len;
function buildMaxHeap(arr) { // 建立大顶堆
len = arr.length;
for (var i = Math.floor(len/2); i >= 0; i--) {
heapify(arr, i);
}
}
function heapify(arr, i) { // 堆调整
var left = 2 * i + 1,
right = 2 * i + 2,
largest = i;
if (left < len && arr[left] > arr[largest]) {
largest = left;
}
if (right < len && arr[right] > arr[largest]) {
largest = right;
}
if (largest != i) {
swap(arr, i, largest);
heapify(arr, largest);
}
}
function swap(arr, i, j) {
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
function heapSort(arr) {
buildMaxHeap(arr);
for (var i = arr.length-1; i > 0; i--) {
swap(arr, 0, i);
len--;
heapify(arr, 0);
}
return arr;
}
算法分析
堆执行一次调整需要O(logn)的时间,在排序过程中需要遍历所有元素执行堆调整,所以最终时间复杂度是O(nlogn)。空间复杂度是O(1)。