1 数据结构学习指南

数据结构核心主要包括:排序,查找,图,树,队列和栈,链表。我们逐一介绍里面的核心内容。

1 概览

(1) 排序包括以下排序

(2)哈希表

(3)查找

2 详细描述

2.1 排序

1 冒泡排序

两两比较,较大的那个数往后走。

2 插入排序

前i-1个数据已经排好序,如果nums[i]<nums[i-1],那么nums[i]就要插入前i-1个数当中。为了方便插入,我们先把nums[i-1]=nums[i], nums[i-1]就空下来了。找到nums[i]的插入位置,插入并且后移数组。

3 希尔排序

插入排序的两个特点:(1)当数组基本有序,插入排序是比较快的,(2)数组很小的时候,插入排序效率比较高。希尔排序就是先把数组分成子序列,先对子序列进行排序,通过多次子序列排序,数组基本有序的时候,再对整个数组进行排序。

4 快速排序

快速排序是分而治之的方法,找到一个枢纽,比它小的数放在它的左边,比它大的数放在它的右边。快排的思想用的比较多,这个算法需要手写一下。

public class QuickSort {

           // TODO Auto-generated method stub

           int pivot = nums[low];

                while (low < high && nums[high] >= pivot) {

                }

                while (low < high && nums[low] <= pivot) {

                }

           }

           return low;

     public static void Qsort(int nums[], int low, int high) {

                int pivot = Partition(nums, low, high);

                Qsort(nums, pivot + 1, high);

     }

           int []nums={49,38,65,97,76,13,27,49};

           for (int i : nums) {

           }

}

     private static int Partition(int[] nums, int low, int high) {

           // 设置一个中值

           while (low < high) {

                     high--;

                nums[low] = nums[high];

                     low++;

                nums[high] = nums[low];

           nums[low]=pivot;

     }

           if (low < high) {

                Qsort(nums, low, pivot - 1);

           }

     public static void main(String[] args) {

           Qsort(nums, 0, nums.length-1);

                System.out.print(i+" ");

     }

5 选择排序

每次从i及其后面的数据中,选择一个最小的放到i的位置。

6 堆排序

堆排序涉及到两个问题:(1)如何建堆 (2)删除了堆顶的元素,然后进行调整。

首先,知道两条定理:(1)树的第一个非终端节点为n/2 (2)i节点的两个孩子分别为2*i 和2*(i+1)

1 堆的调整 (自上而下)

找到非终端节点i。沿着较大的孩子向下筛选,寻找非终端节点的位置。

2 堆排序

每一次都把堆顶元素放到最后,然后,剩余的数构成的堆进行调整。

public class HeapSort {

     public static void HeapAdjust(int nums[], int s, int m) {

           int rc = nums[s];

           for (int j = 2 * s; j < m; j++) {

                // 要顺着较大的孩子往下找

                if (j < m - 1 && nums[j] < nums[j + 1]) {

                     j = j + 1;

                }

                // 找一个合适的位置退出

                if (rc >= nums[j]) {

                     break;

                }

                nums[s] = nums[j];

                // 继续找,开始找较大的孩子的左右子树中合适的位置

                s = j;

           }

           nums[s] = rc;

     }

     public static void Hsort(int nums[]) {

           for (int i = nums.length - 1; i >= 0; i--) {

                HeapAdjust(nums, i, nums.length);

           }

           for (int i = nums.length - 1; i >= 0; i--) {

                int temp = nums[i];

                nums[i] = nums[0];

                nums[0] = temp;

                HeapAdjust(nums, 0, i - 1);

           }

     }

     public static void main(String[] args) {

           int[] nums = { 49, 38, 65, 97, 76, 13, 27, 49 };

           Hsort(nums);

           for (int i : nums) {

                System.out.print(i + " ");

           }

     }

}

7 归并排序

就是把两个数组,合并成一个新的数组,那么时间复杂度就位m+n。

8 基数排序

通过分配和收集进行排序。分配:按照条件把数据分配到各个桶中 合并:每次分配完,进行一次合并

2.2 哈希表

理想的查找,是键值对的方式。但是,这种方式往往难以实现。不过,我们可以趋近这种键值对的方式,这种方式就是哈希表。

1 哈希函数的集中构造方式

(1)直接定址法

(2)数值分析法

(3)平方取中法

(4)除留取余法

(5)折叠法

(6)随机数法

2 就算采取了上边的方法,依然还是会存在冲突的

(1)开放地址法

(2)再哈希

(3)链地址法

(4)建立公共溢出区

链地址法是相当重要的,因为HashMap和HashTable都是采取的这种方法。不过呢,JDK1.8之后呢,当链中的数据超过一定的数量之后,就用红黑树存放链中的数据,红黑树会加快查找的速度。

3 红黑树

二叉平衡树要求左右子树的高度不超过1,而红黑树则要求左右子树深度不超过2倍,放宽了条件。

性质1. 节点是红色或黑色。

性质2. 根节点是黑色。

性质3 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)

性质4. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

通过四个性质,我们得到以下结论

(1)性质4保证了红黑树的左右子树的黑色节点的高度是相等的,平衡的。

(2)再用性质3保证每条路径添加的红色节点的个数最多为黑色节点的个数-1。

(3)每一插入的节点都当作红色节点,如果当作黑色的肯定会违法性质4,就要调整,而红色不一定需求调整

红黑树的构建

(1)如果插入的节点的爹是黑色,直接插入即可

(2)如果插入的节点的爹是红色,分三种情况

2.3 查找

1 折半查找

只针对有序表。low指向开头,high指向结尾,mid指向中间。如果num>nums[mid],那么low=mid+1。反之,high=mid-1。如果,num==nums[mid],则说明找了的num所在的位置为mid。折半还是非常重要的,因此,要写一下算法。

public class Binary {

           int low = 0;

           int mid;

                mid = (low + high + 1) / 2;

                     low = mid + 1;

                     return mid;

                     high = mid - 1;

           }

     }

           int[] nums = { 5, 13, 19, 21, 37, 56, 64, 75, 80, 88, 92 };

     }

     public static int bin(int nums[], int num) {

           int high = nums.length - 1;

           while (low < high) {

                if (num > nums[mid]) {

                } else if (nums[mid] == num) {

                } else {

                }

           return 0;

     public static void main(String[] args) {

           System.out.println(bin(nums, 75));

}

2 二叉查找树

二叉查找树,比较简单的一种树。就是左边节点都小于根节点,右边的节点都大于根节点。

3 二叉平衡树

特殊的二叉查找树,保证其左右子树的高度相差不超过1。要保持这个状态必须经过旋转。

平衡二叉树的定义:

  任意的左右子树高度差的绝对值不超过1,将这样的二叉树称为平衡二叉树,二叉平衡树前提是一个二叉排序树

平衡二叉树的插入:

  二叉平衡树在插入或删除一个结点时,先检查该操作是否导致了树的不平衡,若是,则在该路径上查找最小的不平衡树,调节其平衡。

  4种平衡调整如下(结点的数字仅作标记作用):

  ①LL:右单旋转

  

  ②RR:左单旋转

  

  ③LR平衡旋转:先左后右

  

  ④RL平衡旋转:先右后左

  

平衡二叉树查找:平衡二叉树查找过程等同于二叉排序树相同,因此平衡二叉树查找长度不超过数的长度,及其平均查找长度为O(log2n)。

4 B树

B树也叫B-树。

众所周知,IO操作的效率很低,那么,当在大量数据存储中,查询时我们不能一下子将所有数据加载到内存中,只能逐一加载磁盘页,每个磁盘页对应树的节点。造成大量磁盘IO操作(最坏情况下为树的高度)。平衡二叉树由于树深度过大而造成磁盘IO读写过于频繁,进而导致效率低下。

  所以,我们为了减少磁盘IO的次数,就你必须降低树的深度,将“瘦高”的树变得“矮胖”。一个基本的想法就是:

  (1)、每个节点存储多个元素

  (2)、摒弃二叉树结构,采用多叉树

一个m阶的B树具有如下几个特征:B树中所有结点的孩子结点最大值称为B树的阶,通常用m表示。一个结点有k个孩子时,必有k-1个关键字才能将子树中所有关键字划分为k个子集。

1.根结点至少有两个子女。

2.每个中间节点都包含k-1个元素和k个孩子,其中 ceil(m/2) ≤ k ≤ m

3.每一个叶子节点都包含k-1个元素,其中 ceil(m/2) ≤ k ≤ m

4.所有的叶子结点都位于同一层。

5.每个节点中的元素从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域划分

6.每个结点的结构为:(n,A0,K1,A1,K2,A2,…  ,Kn,An)

    其中,Ki(1≤i≤n)为关键字,且Ki<Ki+1(1≤i≤n-1)。

Ai(0≤i≤n)为指向子树根结点的指针。且Ai所指子树所有结点中的关键字均小于Ki+1。

n为结点中关键字的个数,满足ceil(m/2)-1≤n≤m-1。

示例:三阶B树(实际中节点中元素很多)

查询

  以上图为例:若查询的数值为5:

  第一次磁盘IO:在内存中定位(与17、35比较),比17小,左子树;

  第二次磁盘IO:在内存中定位(与8、12比较),比8小,左子树;

  第三次磁盘IO:在内存中定位(与3、5比较),找到5,终止。

整个过程中,我们可以看出:比较的次数并不比二叉查找树少,尤其适当某一节点中的数据很多时,但是磁盘IO的次数却是大大减少。比较是在内存中进行的,相比于磁盘IO的速度,比较的耗时几乎可以忽略。所以当树的高度足够低的话,就可以极大的提高效率。相比之下,节点中的元素多点也没关系,仅仅是多了几次内存交互而已,只要不超过磁盘页的大小即可。

插入

  对高度为k的m阶B树,新结点一般是插在叶子层。通过检索可以确定关键码应插入的结点位置。然后分两种情况讨论:

  1、 若该结点中关键码个数小于m-1,则直接插入即可。

  2、 若该结点中关键码个数等于m-1,则将引起结点的分裂。以中间关键码为界将结点一分为二,产生一个新结点,并把中间关键码插入到父结点(k-1层)中

  重复上述工作,最坏情况一直分裂到根结点,建立一个新的根结点,整个B树增加一层。

例如:在下面的B树中插入key:4

这里写图片描述

第一步:检索key插入的节点位置如上图所示,在3,5之间;

第二步:判断节点中的关键码个数:

  节点3,5已经是两元素节点,无法再增加。父亲节点 2, 6 也是两元素节点,也无法再增加。根节点9是单元素节点,可以升级为两元素节点。;

第三步:结点分裂:

  拆分节点3,5与节点2,6,让根节点9升级为两元素节点4,9。节点6独立为根节点的第二个孩子。

最终结果如下图:虽然插入比较麻烦,但是这也能确保B树是一个自平衡的树

删除

 在B树上删除关键字k的过程分两步完成(不是最后一层删除,也要把它转化为最后一层删除):

   (1)找出该关键字所在的结点。然后根据 k所在结点是否为叶子结点有不同的处理方法。

   (2)若该结点为非叶结点,且被删关键字为该结点中第i个关键字key[i],则可从指针son[i]所指的子树中

   找出最小关键字Y,代替key[i]的位置,然后在叶结点中删去Y。

  (1)如果被删关键字所在结点的原关键字个数n>=ceil(m/2),说明删去该关键字后该结点仍满足B树的定义。这种情况最为简单,只需从该结点中直接删去关键字即可。

  (2)如果被删关键字所在结点的关键字个数n等于ceil(m/2)-1,说明删去该关键字后该结点将不满足B树的定义,需要调整。

调整过程为:

情况1:如果其左右兄弟结点中有“多余”的关键字,即与该结点相邻的右(左)兄弟结点中的关键字数目大于 ceil(m/2)-1。则可将右(左)兄弟结点中最小(大)关键字上移至双亲结点。而将双亲结点中小(大)于该上移关键字的关键字下移至被删关键字所在结点中。

情况2: 如果左右兄弟结点中没有“多余”的关键字,即与该结点相邻的右(左)兄弟结点中的关键字数目均等于 ceil(m/2)-1。这种情况比较复杂。需把要删除关键字的结点与其左(或右)兄弟结点以及双亲结点中分割二者的关键字合并成一个结点,即在删除关键字后,该结点中剩余的关键字加指针,加上双亲结点中的关键字Ki一起,合并到Ai(是双亲结点指向该删除关键字结点的左(右)兄弟结点的指针)所指的兄弟结点中去。如果因此使双亲结点中关键字个数小于ceil(m/2)-1,则对此双亲结点做同样处理。以致于可能直到对根结点做这样的处理而使整个树减少一层。

下面举一个简单的例子:删除元素11.

这里写图片描述

第一步:判断该元素是否在叶子结点上。

   该元素在叶子节点上,可以直接删去,但是删除之后,中间节点12只有一个孩子,不符合B树的定义:每个中间节点都包含k个孩子,(其中 ceil(m/2) <= k <= m)所以需要调整;

第二步:判断其左右兄弟结点中有“多余”的关键字,即:原关键字个数n>=ceil(m/2) - 1;

  显然结点11的右兄弟节点中有多余的关键字。那么可将右兄弟结点中最小关键字上移至双亲结点。而将双亲结点中小于该上移关键字的关键字下移至被删关键字所在结点中即可

5 B+树

B+树是B树的变种,有着比B树更高的查询效率。下面,我们就来看看B+树和B树有什么不同。

特点

一个m阶的B+树具有如下几个特征:

1.有k个子树的中间节点包含有k个元素(B树中是k-1个元素),每个元素不保存数据,只用来索引,所有数据

都保存在叶子节点。

2.所有的叶子结点中包含了全部元素的信息,及指向含这些元素记录的指针,且叶子结点本身依关键字的大小

自小而大顺序链接。

3.所有的中间节点元素都同时存在于子节点,在子节点元素中是最大(或最小)元素。

查找

       B+树的优势在于查找效率上,下面我们做一具体说明:

  首先,B+树的查找和B树一样,类似于二叉查找树。起始于根节点,自顶向下遍历树,选择其分离值在要查找值的任意一边的子指针。在节点内部典型的使用是二分查找来确定这个位置。

  (1)、不同的是,B+树中间节点没有卫星数据(索引元素所指向的数据记录),只有索引,而B树每个结点中的每个关键字都有卫星数据;这就意味着同样的大小的磁盘页可以容纳更多节点元素,在相同的数据量下,B+树更加“矮胖”,IO操作更少

  B树的卫星数据:

     

  这里写图片描述

  B+树的卫星数据:

  这里写图片描述

  需要补充的是,在数据库的聚集索引(Clustered Index)中,叶子节点直接包含卫星数据。在非聚集索引(NonClustered Index)中,叶子节点带有指向卫星数据的指针。

  (2)、其次,因为卫星数据的不同,导致查询过程也不同;B树的查找只需找到匹配元素即可,最好情况下查找到根节点,最坏情况下查找到叶子结点,所说性能很不稳定,而B+树每次必须查找到叶子结点,性能稳定

  (3)、在范围查询方面,B+树的优势更加明显

  B树的范围查找需要不断依赖中序遍历。首先二分查找到范围下限,在不断通过中序遍历,知道查找到范围的上限即可。整个过程比较耗时。

  而B+树的范围查找则简单了许多。首先通过二分查找,找到范围下限,然后同过叶子结点的链表顺序遍历,直至找到上限即可,整个过程简单许多,效率也比较高。

  例如:同样查找范围[3-11],两者的查询过程如下:

  B树的查找过程:

  这里写图片描述

  B+树的查找过程:

  这里写图片描述

插入

   B+树的插入与B树的插入过程类似。不同的是B+树在叶结点上进行,如果叶结点中的关键码个数超过m,就必须分裂成关键码数目大致相同的两个结点,并保证上层结点中有这两个结点的最大关键码。

删除

  B+树中的关键码在叶结点层删除后,其在上层的复本可以保留,作为一个”分解关键码”存在,如果因为删除而造成结点中关键码数小于ceil(m/2),其处理过程与B-树的处理一样。在此,我就不多做介绍了。

总结

B+树相比B树的优势:

  1.单一节点存储更多的元素,使得查询的IO次数更少;

  2.所有查询都要查找到叶子节点,查询性能稳定;

  3.所有叶子节点形成有序链表,便于范围查询。

发布了146 篇原创文章 · 获赞 91 · 访问量 13万+

猜你喜欢

转载自blog.csdn.net/A1342772/article/details/98760038