数据结构与算法

大O符号:Big O notation,是由德国数论学家保罗·巴赫曼在其1892年的著作《解析数论》首先引入的
指数函数:幂 = 2N
对数函数:指数 = log2N,log10N简写为lgN,logeN简写为lnN
对数的底:logaN中,增长率主要取决于N,因此O(logaN)简写为O(logN)
平均时间复杂度:常数 c、对数 logN、对数平方log2N、线性N、NlogN、平方N2、指数 2N

链表 List
ArrayList:数组实现,查找和更新 O(c),添加和删除 O(N)
LinkedList:双链表实现,查找和更新 O(N),添加和删除 O(c)

栈 Stack
可以把双链表当做栈来操作,即后进先出

队列 Queue
可以把双链表当做队列来操作,即先进先出

树的遍历
先序遍历:先处理本节点,再处理子节点
中序遍历:先处理左子树,在处理本节点,最后处理由子树
后序遍历:先处理子节点,再处理本节点

二叉树
每个枝节点最多有两个子节点,二叉树的平均高度为O(√N),N为节点数

二叉查找树
每个枝节点,它的左子节点小于它,它的右子节点大于它
二叉查找树的节点的平均高度为O(logN),即查找、新增的平均时间为O(logN),最坏O(n)
二叉查找树节点删除:删除A节点的左子节点B,则查找B节点的右子树的最小节点C,来替换B。如果C不是叶节点,则递归删除。
懒惰删除:不实际删除节点,而是标记为假删除

AVL树(取名于两个发明者名字的首字母)
带有平衡条件的二叉查找树,保证每个节点的左子树和右子树的高度最多差1
实现:A有左子节点B,没有右子节点,要给B插入左子节点C,会导致A失衡。让B代替A的位置,A作为B的右子节点。最终为B有左子节点C,右子节点A

B树(Balanced Tree of Order M,多叉平衡搜索树)
1、叶节点是个数组,数据项只保存在叶节点的数组中
2、枝节点保存两组数据,形如[100,200] [子节点1,子节点2,子节点3]
3、子节点1的数据范围在(,100],子节点2的数据范围在[100,200],子节点3的数据范围在[200,)

红黑树
带有颜色标记的二叉查找树,get\add\remove的最坏时间复杂度O(logN)
1、每个节点要么是红色,要么是黑色。根节点是黑色
2、红色节点不能连续
3、任一节点,向下到树末梢的任何路径,都含有相同个数的黑色节点
与二叉查找树对比:任一子树的最长路径不大于最短路径的两倍,保证了查找速度
与AVL树对比:降低了平衡条件,任何不平衡都会在三次旋转之内解决,使得插入和删除的性能更高

散列

散列三要素
散列函数:输入key,输出一个散列值,例如取余函数
散列表:存放数据的空间
散列冲突解决方案:解决多条数据的散列值相同的问题

分离链接法(拉链法):把散列值相同的数据放到链表中
线性探测法:一条数据在散列表上的位置已经被占据,则把他放到下一个存储单元
双散列:一条数据在散列表上的位置已经被占据,则把它放到2倍散列值的位置上

JAVA里的散列实现
数组作为散列表,数组的一项作为桶,拉链法解决散列冲突。hashCode()相同的放入一个桶,hashCode()相同并equals()的,当做重复元素。数组有一个初始长度,数据量到达阈值时,扩容resize到原来的两倍,并重新散列rehash。除非发生扩容,否则操作时间复杂度O(1),比红黑树快。
以HashMap为例,构造时可以传入两个参数initialCapacity和loadFactor,即初始容量和扩容阈值,默认为16和0.75。最佳初始容量为 预期数据量 / loadFactor + 1。

无重复集合 Set
TreeSet:红黑树实现,有序,元素需要实现Comparable接口的compareTo方法,不能放入null
HashSet:散列实现,可以放入一个null
LinkedHashSet:散列表存放数据,链表记录插入顺序,顺序与TreeSet不同

映射 Map
TreeMap:红黑树实现
HashMap:散列实现,key、value都可以为null。
Hashtable:散列实现,key、value都不可以为null。每个操作方法都声明了Synchronize,线程安全,同时性能也稍低。
ConcurrentHashMap:散列实现,线程安全,迭代时只锁死部分元素,比Hashtable性能稍高

优先队列 priority queue
优先队列通常用二叉堆来实现,因此二叉堆可以指代优先队列,简称堆

二叉堆 binary heap
完全二叉树:除了最后两层,所有节点都有两个子节点,且最底层节点从左到右紧密排列。可以用树实现,可一样用数组实现。
二叉堆:一个完全二叉树,它的每棵子树的根节点都是这个子树的最小元素
PriorityQueue:数组式二叉堆实现

排序(以下排序说的都是升序)

双循环法

冒泡排序 平均O(n2),最坏O(n2)

// 任一时刻,数组的后部分已排好序
for (var i = 0; i < len; i++) {
 for (var j = 0; j < len - i - 1; j++) {   // 遍历前部分
  if (arr[j] > arr[j+1]) {                   // 对比相邻元素,让大元素往后冒泡,加入到后部分
   var temp = arr[j+1];  
   arr[j+1] = arr[j];
   arr[j] = temp;
  }
 }
}

选择排序 平均O(n2),最坏O(n2)

var len = arr.length;
// 任一时刻,数组的前部分已排好序
for (var i = 0; i < len - 1; i++) {
 var minIndex = i;
 for (var j = i + 1; j < len; j++) {  // 遍历后部分,找到最小值
  if (arr[j] < arr[minIndex]) {
    minIndex = j; 
  }
 }
 var temp = arr[i];                   // 把找到的最小值加到前部分
 arr[i] = arr[minIndex];
 arr[minIndex] = temp;
}

插入排序 平均O(n2),最坏O(n2)

// 任一时刻,数组的前部分已排好序
for (var i = 1; i < array.length; i++) {
 var cur = array[i];
 for(var j=i-1; array[ j ] > cur; j-- ) {    // 从后往前遍历前部分,将当前元素插入到前部分
   array[ j + 1] = array[ j ];            // 比当前元素大的都往后移动一格
 }
 array[ j + 1] = cur;      // 将当前元素插入
}
分组法

希尔排序 平均O(nlogn),最坏O(nlog2n)

var len = arr.length, gap = 1;
while(gap < len / 5) {  // 动态定义初始间隔
  gap =gap*5+1;    // 此计算为实践经验,gap和length的关系为 [1,1-5] [6, 6-30] [31, 31-155]
}
while (gap > 0) {
   // 位置相差gap的元素分为一组,对每组进行插入排序
 for (var i = gap; i < len; i++) {  // i 每加1,就进入到了另外一组,所以对每组的排序 是 穿插进行的
  var temp = arr[i];
  for (var j = i-gap; j >= 0 && arr[ j ] > temp; j-=gap) {
   arr[ j+gap ] = arr[ j ];
  }
  arr[ j+gap ] = temp;
 }
    gap = Math.floor(gap / 5);  // gap最后一次是 1,就是一个插入排序,但此时元素移动可以大大减少
}

归并排序 平均O(nlog n),最坏O(nlog n)

function mergeSort(arr) {
  var len = arr.length;
  if(len < 2) {      // 拆分到只有一个元素时,就当做是排好序的数组
    return arr;
  }
  var middle = Math.floor(len / 2),   // 把数组拆分成两半
  left = mergeSort(arr.slice(0, middle)), 
  right = mergeSort(arr.slice(middle));  // 两半分别排好序
  return merge(left, 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(n*log n),最坏O(n2)

function quickSort(array, start, end) {
  if (start < end) {       // 当 start 等于end 时,就是拆分到了一个元素
    var last = array[end],          // 用最后一个元素 跟 其他元素比较
                    i = start - 1;                   // 标记比last小的元素存放的位置,目前还没有,所以是 -1
    for (var j = start; j <= end; j++) {   // 遍历整个数组
      if (array[ j ] <= last ) {   // 当前元素小于等于last,则交换到位置 i
        i++;
        var temp = array[ i ];
        array[ i ] = array[ j ];
        array[ j ] = temp;
      }
    }
              // 将数组拆分成两个数组,分别排序,其中一个数组都小于等于last,另一个都大于last 
    quickSort(array, start, i - 1);  // start 到 i - 1都小于等于last
              // 中间还有个array[ i ]就是 last
    quickSort(array, i + 1, end);  // i + 1到end都大于last
  }
  return array;
}
quickSort(arr,0,arr.length-1);
分桶法

以下k表示桶数

计数排序 平均O(n+k),最坏O(n+k)
把数据的值作为下标,放到一个新数组中,最后只要顺次读取新数组
限制:桶的数量(新数组的长度)不小于数据的取值范围

桶排序 平均O(n+k),最坏O(n2)
1、遍历数组,将相同级别的数据放到一个桶中
2、分别排序每一个桶
3、读出每一个桶的数据
例如:100以内数字的排序,根据数字的十位数,分别放到编号0-9的数组中,分别对每个数组排序,依次读出每个桶里的数字

基数排序 平均O(nk),最坏O(nk)
1、遍历数组,将相同尾数的数据放到一个桶中
2、按顺序读出所有数据,再根据次尾数分别放到桶中,以此类推
3、按顺序读出所有数据
例如:对[12,11,2,1]排序,放入两个桶[11,1][12,2],读出[11,1,12,2],再次入桶[1,2][11,12],再次读出[1,2,11,12]

二叉树法

堆排序 平均O(nlog n),最坏O(nlog n)
即每次从二叉堆中取出堆顶元素

常见问题与方案

topK

问题:从N个数中,找到最大的K个数
方案:读取前K个数,创建一个堆顶为最小数的堆;读取后N-K个数,比堆顶大则删除堆顶,并插入堆;时间复杂度为O(N*logK)

寻找中位数

等同于找出 top N/2

一个整数的二进制表示中1的个数

while(n>0){
if((n&1)==1){result++}
n = n>>1 // 每次消除一位
}

while(n>0){
result ++;
n = n&(n-1) // 每次消除一个1
}

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