排序算法与常见数据结构

排序:

一、插入类排序  

1、直接插入排序

{49,38,65,97,76,13,27,_49}

1)插49

49         38,65,97,76,13,27,_49

2)插38

38,49         65,97,76,13,27,_49

以此类推。。。

2、折半插入排序  o(nlogn)

lowhigh   

3、希尔排序  (nlogn)

又称缩小增量排序,以增量分割序列,

原始序列:49,38,65,97,76,13,27,_49,55,04

以增量5分割序列得到子序列,然后对子序列进行排序得到一趟希尔排序结果

二、交换类排序

1、冒泡排序   o(n^2)

void BubbleSort(int r[n],i
  int i,j,flag,temp;
  for(i=n;i>=2;i--){  //比较个数
    flag=0;
    for(j=2;j<=i;j++){
      if(r[j-1]>r[j]){
        temp=r[j];
        r[j]=r[j-1];
        r[j-1]=temp;
        flag=1;
      }
      if(flag==0){return}
    }
  }
}

2、快速排序  “枢纽”     最好的情况o(nlogn)     最坏的情况o(n*n)

基本思想:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据比另一部分的所有数据都要小,然后再按此方法对这两部分分别进行快排,可以递归进行。

时间复杂度分析:

  最好的情况:枢纽选取得当,每次都能均匀的划分两个序列    T(n)=2T(n/2)+f(n)=2(2T(n/4)+f(n/2))+f(n)

  最坏的情况:枢纽为最大或最小的数字,其他数都被划分到一个序列去了  相当于冒泡排序了

  枢纽可以选择任何位置,一般为开始、结束以及中间位置节点。

  每次递归需要比较n次,递归深度为logn,所以需要比较logn次,所以时间复杂度是O(nlogn)

代码:

function quickSort(arr){
    if(arr.length<=1)return arr;
    var left=[], right=[];
    var mid=arr.splice(Math.floor(arr.length/2),1);
    for(var i=0;i<arr.length;i++){
        if(arr[i]<mid){
            left.push(arr[i]);
        }
        if(arr[i]>mid){
            right.push(arr[i]);
        }
    }
    return  quickSort(left).concat(mid,quickSort(right));//递归
}
var array=[2,4,3,6,5,8,7,23,13];
alert(quickSort(array));

三、选择类排序

1、简单选择排序  o(n^2)

从头到尾开始扫描,找出最小的 一个元素,和第一个元素交换,接着从剩下的元素中继续选择和交换

2、堆排序  o(logn)

堆是具有以下性质的完全二叉树:每个节点的值都大于或等于其左右孩子节点的值,称为大顶堆;或者每个节点的值都小于或等于其左右孩子节点的值,称为小顶堆。

function heapSort(arr){    
    for(var i=Math.floor(arr.length/2)-1;i>=0;i--){//从第一个非叶子节点开始比较
        heapAdjust(arr,i,arr.length);
    }

    for(var j=arr.length-1;j>0;j--){
        swap(arr,0,j);//调整为大根堆之后交换第一个和最后一个节点
        heapAdjust(arr,0,j);//从头再开始调整
    }
    return arr;
}
            
function heapAdjust(arr,i,len){
    var temp=arr[i];
    var largest=i;//父节点
    var left=2*i+1;//左孩子
    var right=2*i+2;//右孩子
            
    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);//交换位置
        heapAdjust(arr,largest,len);//交换后的子节点有可能还会比孩子节点小,所以依次比较
    }
}
function swap(arr,i,largest){
    var temp=arr[i];
    arr[i]=arr[largest];
    arr[largest]=temp;
}

var arr=[2,4,3,6,5,8,7,23,13];
console.log(heapSort(arr));//[2, 3, 4, 5, 6, 7, 8, 13, 23]

堆的时间复杂度分析:

堆排序包括两个过程:初始化建堆和重建堆。所以堆排序的时间复杂度悠这两部分组成。

一、初始化建堆。只需要对非叶子节点进行调整。假设高度为K。

总的时间为s=2^(i-1)*(k-i),其中i表示第几层,2^(i-1)表示该层上有多少个节点,(k-i)表示子树上要下调比较的次数。

所以s=2^(k-2)*1+2^(k-3)*2+....+2^0 *(k-1)  =2^k-k-1;====》因为叶子层不用交换,所以i从k-1层开始到1;

又因为k为完全二叉树的深度,所以k=log(n),所以s=n-log(n)-1   时间复杂度为O(n)

 二、重建堆

在取出堆顶点和最后一个节点交换后,需要对堆进行重建。每次重建意味着有一个节点出堆,所以堆的容量减1,随着堆的容量减少,层数会下降。重建堆一共需要n-1次循环,每次循环的比较次数为log(i),相加为log(2)+log(3)+...+log(n)=log(n!)与nlog(n)同阶。

 

 二分查找的时间复杂度分析:O(logn)

二分查找每次查询都是从数组中间切开,剩余查询数目为上次的一半。

所以假设我们查找a元素需要查找k次

第几次查询                   剩余待查元素数量

1                                            n/2

2                                           n/(2^2)

3                                           n/(2^3)

: 

k                                          n/(2^k)

第k次查找到a元素,所以当前数组数量为1,即只有a,所以n/(2^k)=1;     k=logn

稳定性:在排序之前,有两个数相等,但是在排序结束之后,它们可能会改变顺序。只要有这个可能性就是不稳定的。

 常用数据结构:

数据结构是指相互之间存在一种或多种相互关系的数据元素的集合。

线性表、栈和队列、树、图、堆、HashMap。

一、线性表

线性表是最常用且最简单的一种数据结构,它是n个数据元素的有限序列。

线性表有两种:数组和链表

(1)数组是用一组连续的存储单元依次存储线性表的数据元素, 是一种大小固定的数据结构,数组一旦创建后大小就不能改变了。

优点:1.可以通过下标高效地访问数组

缺点:1.数组的大小固定后无法改变

   2.数组只能存储一种类型地数据

           3。添加、删除的操作慢,因为要移动其他元素

(2)链表

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

每次添加的next或删除元素时,只要改变相关节点的next指向就可以,效率很高。

使用场景:

(1)数据量较小

(2)不需要事先知道数据规模;

(3)适用于频繁的插入操作。

缺点:只能顺序查找,时间复杂度为O(1)

二、栈(后进先出)

插入和删除操作只能在栈顶进行,该位置在表的末端。对于栈的基本操作是入栈和出栈。

三、队列(先进先出)

只允许在表的前端(队头)进行删除操作,在表的末尾(队尾)进行插入操作。

栈和队列的区别:

  栈的插入和删除都是在一端进行的,而队列的操作是在两端进行的;

  栈是先进后出,队列是先进先出;

  栈只允许在栈顶进行插入和删除;队列是在队尾插入元素,在队头删除元素;

四、树

树形结构是非线性数据结构,以树和二叉树最常见。

二叉树:每个节点最多拥有两棵子树,并且二叉树的子树有左右之分,次序不能任意颠倒。

二叉树的性质:

(1)若二叉树的层次从0开始,则在二叉树的第i层最多有2^i个节点;

(2)高度为k的二叉树最多有2^(k+1)-1个节点;

(3)对任意一棵二叉树,如果其叶子节点(度为0)数为m,度为2的节点数为n,则m=n+1;

二叉树的种类:

(1)二叉搜索树(二叉排序树)(BST)

  根节点大于等于左子树,小于等于右子树。

(2)平衡二叉树 (AVL)

  平衡二叉树是有如下性质的二叉排序树:

  • 左右子树的深度之差的绝对值不超过1;
  • 左右子树仍然为平衡二叉树;

(3)红黑树(RBT)

   平衡二叉树是严格平衡树,因此在增加或者删除节点的时候,根据不同的情况,旋转的次数比红黑树要多;

  红黑树是一种自平衡二叉排序树,通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保从根到叶子节点的最长路径不会是最短路径的两倍,用非严格的平衡来换取增删节点时候旋转次数的降低,任何不平衡都会在三次旋转之内解决。

  红黑树是弱平衡树,用非严格的平衡来换取删除节点时候旋转次数的降低;所以如果搜索次数远远大于插入和删除,选择AVL树;如果搜索、插入删除次数差不多,应选择红黑树。

  红黑树的应用:

    

   接下来我们依次插入如下五个节点:7,6,5,4,3。依照二叉查找树的特性,结果会变成什么样呢?以下形态虽然符合二叉排序树的特性,但是查找性能大打折扣,几乎变成线性的。所以如何解决多次插入新节点导致的不平衡呢?红黑树就派上用场了。

 红黑树的特性:

  • 节点是红色或黑色的;
  • 根节点是黑色;
  • 每个叶子节点都是黑色的空节点;
  • 每个红色节点的两个子节点都是黑色的(从每个叶子到根的所有路径不能有两个连续的红色节点)
  • 任何一个节点到其每个叶子节点的所有路径都包含相同数目的黑色节点。

下面是一棵典型的红黑树

 当插入或删除节点时,红黑树的规则可能会被打破,这是需要做一些调整,来维持规则。

调整有两种方法:变色和旋转。

(4)满二叉树

  除叶子节点外的每一个节点都有两个孩子,即每一层都被填满。

(5)完全二叉树

  除最后一层之外的其他每一层都被完全填满,并且所有节点都保持向左对齐。(即按照满二叉树的结构排列)

五、图

借助一维数组实现静态链表

  首先让数组的元素都是由两个数据域组成,data和cur,也就是说数组的每个下标都对应一个data和一个cur。

  数据域的data用来存放数据元素,而游标cur相当于单链表的next指针,存放的是该元素的后继元素在数组中的下标。我们把这种用数组描述的链表叫做静态链表。

六、哈希表

根据给定的关键字来计算出关键字在表中的地址。

常用Hash函数的构造方法

(1)直接定址法

取关键字或者关键字的某个线性函数为hash地址,即H(key)=key或者H(key)-a*ky+b;

(2)数字分析法

(3)平方取中法

(4)除留余数法

取关键字被某个不大于hash表表长m的数p除后所得的余数为hash地址。H(key)=key mod p (p<=m) p一般是小于或等于表长的最大素数。

创建hash表发现会有很多个关键字共用一个地址,所以需要处理冲突。

解决冲突的几种方法:

(1)线性探测法

从发生冲突的地址开始,依次探测下一个地址,(当到达m-1表尾时,下一个探测的地址就是表首地址),直到找到一个空地址为止

(2)平方探测法  依次查找(i+1)^2,(i-1)^2、(i+2)^2、(i-2)^2是否为空

(3)链地址法

查找成功和不成功时的平均查找长度

查找成功时的平均查找长度ASL1:关键是求出对于每个关键字所对应的比较次数。如果没有冲突则只需要比较一次,如果发生冲突,则计算比较次数。

ASL1=(所有关键字的比较次数之和)/长度;

七、HashMap

HashMap结构:添加和删除节点操作时间复杂度都为O(1),因为可以直接通过计算(如除留余数法)求的地址。

HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。

数组:存储区域连续,占用内存严重,寻址容易,插入删除困难;

链表:存储区间离散,占用内存比较宽松,寻址困难,插入删除也容易。

HashMap存储原理:

先声明一个范围较大的数组来存储元素。另外设计一个哈希函数来获取每个元素的key(关键字)的函数值(下标)相对应,数据存储的元素是一个Entry类,这个类有三个数据域:key、value、next(指向下一个Entry)

八、最小生成树

(1)普里姆算法

  从图中任取一个顶点,把它当成一棵树,然后从与这棵树相接的边中选取一条最短(权值最小)的边,并将这条边及其所连接的顶点也并入这棵树中,此时得到了一棵有两个顶点的树。然后从与这棵树相接的边中选取一条最短的边,并将这条边及其所连顶点并入树中,以此类推,直到图中所有的顶点都被并入树中为止。

                       

(2)克鲁斯卡尔

  每次找出候选边中权值最小的边,并将该边并入生成树中,重复此过程直到所有边都检测完为止。

                        

九、最短路径

(1)迪杰斯特拉算法  (广度优先遍历)

原文地址:https://www.cnblogs.com/xiaoan0705/p/8628853.html