数据结构与算法

247 阅读13分钟

时间复杂度

blog.csdn.net/qq_41523096…

用来描述一个函数执行所需要的时长

取一个函数运行时长的最高阶项

根据处理数据的大小,是一个线性的值,用O来标识

比如
T(n) = 3n 
最高阶项为3n,省去系数3,转化的时间复杂度为:
T(n) =  O(n)

O(1),O(nlogn), O(n^3), O(m*n),O(2^n),O(n!)

复杂度介绍图标

链表

优点

  • 一个链表是数据元素的线性集合
  • 每个元素指向下一个元素
  • 这种结构允许在迭代期间有效地从序列中的任何位置插入或删除元素(没有索引所以对其他数据不会有影响) 缺点
  • 访问时间是线性的(难以管道化??
  • 更快的访问,如随机访问,是不可行的。与链表相比,数组具有更好的缓存位置
function ListNode(x){
    this.val = x;
    this.next = null;
}
链表数据结构
{
	"val": 1,
	"next": {
		"val": 2,
		"next": {
			"val": 3,
			"next": null
		}
	}
}

双向链表

优点

  • 由一组称为节点的顺序链接记录组成的链接数据结构
  • 它可以被概念化为两个由相同数据项组成的单链表,但顺序相反
  • 两个节点链接允许在任一方向上遍历列表

缺点

  • 添加或者删除节点时,需做的链接更改要比单向链表复杂得多

排序算法

常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等

冒泡排序

  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

当输入的数据已经是正序时最快

当输入的数据是反序时最慢

function bubbleSort(arr) {
    var len = arr.length;
    for (var i = 0; i < len - 1; i++) {
        for (var j = 0; j < len - 1 - i; j++) {
            var flag = true;
            num++
            if (arr[j] > arr[j + 1]) { // 相邻元素两两对比
                flag = false;
                var temp = arr[j + 1]; // 元素交换
                arr[j + 1] = arr[j];
                arr[j] = temp;
            }
            if (flag) break;
        }
    }
    return arr;
}

时间复杂度:O(n^2) 空间复杂度:O(1)

选择排序

  1. 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
  2. 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
  3. 重复第二步,直到所有元素均排序完毕。

时间复杂度:O(n^2)

空间复杂度:O(1)

/**
 * 选择排序法
 * @param {Array} arr
 * 每次遍历余下的所有数据,将最小的值放到新的数组中
 */
function selectionSort(arr) {
  let temp,minIndex;
  for (let i = 0; i < arr.length - 1; i++) {
    minIndex = i;
    for (let j = i + 1; j < arr.length; j++) {
        if(arr[j]<arr[minIndex]){
            minIndex = j;
        }
    }
    // 获取了最小值的索引
    temp = arr[i]
    arr[i] = arr[minIndex];
    arr[minIndex] = temp;
  }
  return arr;
}

插入排序

  1. 将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
  2. 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)

性能和冒泡排序法一样,是一种优化排序算法

function insertionSort(arr) {
  // 前一个值得索引和当前排序的值
  let preIndex, current; 
  for (let i = 1; i < arr.length; i++) {
    preIndex = i - 1;
    current = arr[i];
    // 当前选中的值依次和前一个值进行比较
    while (current < arr[preIndex] && preIndex >= 0) {
      // 如果小于前一个值,则前一个值往后移动一位
      arr[preIndex + 1] = arr[preIndex];
      preIndex--;
    }
    // 直到当前值大于比较值,或者索引为0
    // 将当前值插入到该索引位置
    arr[preIndex + 1] = current;
  }
  return arr;
}

希尔排序

一种针对插入排序的优化排序算法

希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。

  1. 选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
  2. 按增量序列个数 k,对序列进行 k 趟排序;
  3. 每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
function shellSort(arr) {
    // 先定义一个步长
    let gap = ~~(arr.length / 2);
    // 以这个步长进行插入排序
    for (gap; gap > 0; gap = ~~(gap / 2)) {
        // 每次排序完步长减半直到步长为1结束
        for (var i = gap; i < arr.length; i++) {
            const temp = arr[i];
            for (var j = i - gap; j >= 0 && arr[j] > temp; j -= gap) {
                // 如果满足j就向前移动gap个索引
                console.log(111);
                
                arr[j + gap] = arr[j];
            }
            // 找到了不满足条件的索引的位置,在该位置前一项进行插入操作
            arr[j + gap] = temp;
        }
    }
    return arr;
}

归并排序

将两个(或两个以上)有序表合并成一个新的有序表 先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。

算法实现

function mergeSort(arr) {
    let len = arr.length;
    if (len == 1) return arr;
    let l = arr.slice(0, len / 2);
    let r = arr.slice(len / 2, len)
    return merge(mergeSort(l), mergeSort(r));
}

function merge(l, r) {
    // 合并两个有序数据
    let tempArr = [];
    while (l.length > 0 && r.length > 0) {
        let val;
        if (l[0] > r[0]) {
            val = r.shift();
        } else {
            val = l.shift();

        }
        tempArr.push(val);
    }
    // 将剩余的数据一次性添加到最后
    tempArr.push(...l.length > 0 ? l : r);
    return tempArr;
}

快速排序

  1. 从数列中挑出一个元素,称为 “基准”(pivot);
  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
  3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;

递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会退出,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。

算法实现

function quickSort(arr, left, right) {
    var len = arr.length,
        partitionIndex,
        // left默认从0开始,right默认从末尾开始
        left = typeof left != 'number' ? 0 : left,
        right = typeof right != 'number' ? len - 1 : right;

    if (left < right) {
        // 获取基准值的索引
        partitionIndex = partition(arr, left, right);
        quickSort(arr, left, partitionIndex - 1);
        quickSort(arr, partitionIndex + 1, right);
    }
    // 操作的是同一个数组,所以最后只要把这个数组return出来
    return arr;
}

function partition(arr, left, right) { // 分区操作
    var pivot = left, // 设定基准值(pivot)
        // 小于基准的索引,只有当发现有值小于基准才会与其交换
        // 这样保证,基准左边的值比基准都要小
        index = pivot + 1;
    for (var i = index; i <= right; i++) {
        // 如果小于基准值
        let arrI = arr[i];
        let arrP = arr[pivot];
        if (arr[i] < arr[pivot]) {
            // 应该被放到基准值的左边
            //i 为从左开始,当前遍历到的点
            // index 为基准值的后一位
            swap(arr, i, index);
            index++;
        }
    }
    // 最后将基准与最后一位找到的小于基准的值进行交换
    swap(arr, pivot, index - 1);
    return index - 1;
}
// 进行数组的索引交换
function swap(arr, i, j) {
    var temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

堆排序

  1. 将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
  2. 将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
  3. 重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。

一般升序采用大顶堆,降序采用小顶堆 大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
www.cnblogs.com/chengxiao/p…

桶排序

  1. 什么时候最快-> 当输入的数据可以均匀的分配到每一个桶中。

  2. 什么时候最慢-> 当输入的数据被分配到了同一个桶中。

算法实现

function bucketSort(arr, bucketSize) {
    if (arr.length === 0) {
        return arr;
    }

    var i;
    var minValue = arr[0];
    var maxValue = arr[0];
    for (i = 1; i < arr.length; i++) {
        if (arr[i] < minValue) {
            minValue = arr[i]; // 输入数据的最小值
        } else if (arr[i] > maxValue) {
            maxValue = arr[i]; // 输入数据的最大值
        }
    }

    //桶的初始化
    var DEFAULT_BUCKET_SIZE = 5; // 设置桶的默认数量为5
    // 一个桶的容积
    bucketSize = bucketSize || DEFAULT_BUCKET_SIZE;
    // 利用最大值和最小的差计算出最多需要几个桶
    // 计算出需要几个桶
    var bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1;
    // 初始化桶数组
    var buckets = new Array(bucketCount);
    for (i = 0; i < buckets.length; i++) {
        buckets[i] = [];
    }

    //利用映射函数将数据分配到各个桶中
    // (排序数组-最小值)/桶的个数
    // 将数据依次放到每个桶中
    // 根据桶的索引桶里的数据依次增大
    for (i = 0; i < arr.length; i++) {
        // 计算出当前值应该放在第几个桶中
        buckets[Math.floor((arr[i] - minValue) / bucketSize)].push(arr[i]);
    }

    arr.length = 0;
    for (i = 0; i < buckets.length; i++) {
        insertionSort(buckets[i]); // 对每个桶进行排序,这里使用了插入排序
        for (var j = 0; j < buckets[i].length; j++) {
            // 将每个桶按照索引拼接在一起
            arr.push(buckets[i][j]);
        }
    }
    return arr;
}

function insertionSort(arr) {
    var len = arr.length;
    var preIndex, current;
    for (var i = 1; i < len; i++) {
        preIndex = i - 1;
        current = arr[i];
        while (preIndex >= 0 && arr[preIndex] > current) {
            arr[preIndex + 1] = arr[preIndex];
            preIndex--;
        }
        arr[preIndex + 1] = current;
    }
    return arr;
}

基数排序

思路:

  1. 桶按照从小到大排序,每次位数排序的时候,当前位数,先被倒出的数一定小于后被倒出的数
  2. 按照这个思路进行排序
  3. 第一次,小于10的数被按照从小到大的顺序穿插在数组中
  4. 第二次,小于100的数,被顺序排列
  5. 最终所有的数都会被顺序排列

算法实现

//LSD Radix Sort
var counter = [];

function radixSort(arr, maxDigit = 9) {
    debugger
    var mod = 10;
    var dev = 1;
    // maxDigit数组中最大值的位数
    for (var i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
        for (var j = 0; j < arr.length; j++) {
            // 第一次取个位的值
            // 第二次取十位的值
            // 以此类推
            var bucket = parseInt((arr[j] % mod) / dev);
            if (counter[bucket] == null) {
                counter[bucket] = [];
            }
            // 依据个位的值依次将数组中的每个值放到桶中
            counter[bucket].push(arr[j]);
        }
        var pos = 0;
        // 依次将每个桶中的数据,按照队列的形式倒出
        for (var j = 0; j < counter.length; j++) {
            var value = null;
            if (counter[j] != null) {
                while ((value = counter[j].shift()) != null) {
                    arr[pos++] = value;
                }
            }
        }

        console.log(arr);
        
    }
    return arr;
}

常见算法

贪心算法

在对问题求解时,总是做出在当前看来是最好的选择。

不是对所有问题都能得到整体最优解,但对范围相当广泛的许多问题他能产生整体最优解或者是整体最优解的近似解。

分治算法

分治算法的基本思想是将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,就可得到原问题的解。

分治法解题的一般步骤:

  1. 分解,将要解决的问题划分成若干规模较小的同类问题;
  2. 求解,当子问题划分得足够小时,用较简单的方法解决;
  3. 合并,按原问题的要求,将子问题的解逐层合并构成原问题的解。

动态规划

动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。

与分治法最大的差别是:适合于用动态规划法求解的问题,经分解后得到的子问题往往不是互相独立的(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解)

回溯法

回溯法(探索与回溯法)是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。

基本思想:

回溯法在问题的解空间树中,按深度优先策略,从根结点出发搜索解空间树。算法搜索至解空间树的任意一点时,先判断该结点是否包含问题的解。如果肯定不包含(剪枝过程),则跳过对该结点为根的子树的搜索,逐层向其祖先结点回溯;否则,进入该子树,继续按深度优先策略搜索。 回溯法就是对隐式图的深度优先搜索算法 回溯法:为了避免生成那些不可能产生最佳解的问题状态,要不断地利用限界函数(bounding function)来处死(剪枝)那些实际上不可能产生所需解的活结点,以减少问题的计算量。具有限界函数的深度优先生成法称为回溯法。(回溯法 = 穷举 + 剪枝)

一般步骤:

(1)针对所给问题,定义问题的解空间; (2)确定易于搜索的解空间结构; (3)以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。

两个常用的剪枝函数:

(1)约束函数:在扩展结点处减去不满足约束的子数 (2)限界函数:减去得不到最优解的子树

  用回溯法解题的一个显著特征是在搜索过程中动态产生问题的解空间。在任何时刻,算法只保存从根结点到当前扩展结点的路径。如果解空间树中从根结点到叶结点的最长路径的长度为h(n),则回溯法所需的计算空间通常为O(h(n))。而显式地存储整个解空间则需要O(2^h(n))或O(h(n)!)内存空间。   

分支限界法

www.cnblogs.com/fengty90/p/…