第六天----数据结构笔记

笔记参考文章

1、图

1.图根据边是否有方向,将图可以划分为:无向图和有向图。
2.一条边上的两个顶点叫做邻接点。

3.在无向图中,某个顶点的度是邻接到该顶点的边(或弧)的数目。
4.在有向图中,顶点的度=入度+出度。
–顶点的入边,是指以该顶点为终点的边。
而顶点的出边,则是指以该顶点为起点的边。
–某个顶点的入度,是指以该顶点为终点的边的数目。
而顶点的出度,则是指以该顶点为起点的边的数目。

5.路径:如果顶点(Vm)到顶点(Vn)之间存在一个顶点序列。
则表示Vm到Vn是一条路径。
–路径长度:路径中"边的数量"。
–简单路径:若一条路径上顶点不重复出现,则是简单路径。

6.回路:若路径的第一个顶点和最后一个顶点相同,则是回路。
–简单回路:第一个顶点和最后一个顶点相同,其它各顶点都不重复的回路则是简单回路。

7.连通分量:非连通图中的各个连通子图称为该图的连通分量。
----对无向图而言,
任意两个顶点之间都存在一条无向路径,
则称该无向图为连通图。
----对有向图而言,
若图中任意两个顶点之间都存在一条有向路径,
则称该有向图为强连通图。

8.邻接矩阵是指用矩阵来表示图。
----它是采用矩阵来描述图中顶点之间的关系(及弧或边的权)。
–通常采用两个数组来实现邻接矩阵:
一个一维数组用来保存顶点信息,
一个二维数组来用保存边的信息。
–邻接矩阵的缺点就是比较耗费空间。

9.邻接表是图的一种链式存储表示方法。
----它是改进后的"邻接矩阵",
–它的缺点是不方便判断两个顶点之间是否有边,
但是相对邻接矩阵来说更省空间。

2、图的深度优先搜索(Depth First Search)

和树的先序遍历比较类似,是一个递归的过程。
----它的思想:假设初始状态是图中所有顶点均未被访问,
则从某个顶点v出发,首先访问该顶点,
然后依次从它的各个未被访问的邻接点出发深度优先搜索遍历图,
直至图中所有和v有路径相通的顶点都被访问到。
若此时尚有其他顶点未被访问到,
则另选一个未被访问的顶点作起始点,
重复上述过程,直至图中所有顶点都被访问到为止。

3、广度优先搜索算法(Breadth First Search)

又称为"宽度优先搜索"或"横向优先搜索",简称BFS。
是以v为起点,由近至远,
依次访问和v有路径相通且路径长度为1,2…的顶点。
----它的思想是:从图中某顶点v出发,
在访问了v之后依次访问v的各个未曾访问过的邻接点,
然后分别从这些邻接点出发依次访问它们的邻接点,
并使得“先被访问的顶点的邻接点先于后被访问的顶点的邻接点被访问,
直至图中所有已被访问的顶点的邻接点都被访问到。
如果此时图中尚有顶点未被访问,
则需要另选一个未曾被访问过的顶点作为新的起始点,
重复上述过程,直至图中所有顶点都被访问到为止。

4、拓扑排序算法

  1. 构造一个队列Q(queue) 和 拓扑排序的结果队列T(topological);
  2. 把所有没有依赖顶点的节点放入Q;
  3. 当Q还有顶点的时候,执行下面步骤:
    1 )从Q中取出一个顶点n(将n从Q中删掉),并放入T(将n加入到结果集中);
    2 )对n每一个邻接点m(n是起点,m是终点);
    2.1 )去掉边;
    2.2 )如果m没有依赖顶点,则把m放入Q;
    (顶点A没有依赖顶点,是指不存在以A为终点的边。)

5、克鲁斯卡尔(Kruskal)算法,求加权连通图的最小生成树的算法。

----在含有n个顶点的连通图中选择n-1条边,
构成一棵极小连通子图,
并使该连通子图中n-1条边上权值之和达到最小
则称其为连通网的最小生成树。

–基本思想:按照权值从小到大的顺序选择n-1条边,
并保证这n-1条边不构成回路。
–具体做法:首先构造一个只含n个顶点的森林,
然后依权值从小到大从连通网中选择边加入到森林中,
并使森林中不产生回路,直至森林变成一棵树为止。

1.基本定义

//邻接矩阵边对应的结构体
private static class EData {
    char start; // 边的起点
    char end;   // 边的终点
    int weight; // 边的权重

    public EData(char start, char end, int weight) {
        this.start = start;
        this.end = end;
        this.weight = weight;
    }
};
//邻接矩阵对应的结构体。
public class MatrixUDG {

    private int mEdgNum;        // 边的数量
    private char[] mVexs;       // 顶点集合
    private int[][] mMatrix;    // 邻接矩阵,保存矩阵信息的二维数组。
    private static final int INF = Integer.MAX_VALUE;   // 最大值
    //mMatrix[i][j]=1,
    //则表示"顶点i(即mVexs[i])"和"顶点j(即mVexs[j])"是邻接点;
    //mMatrix[i][j]=0,
    //则表示它们不是邻接点。
    ...
}

2.克鲁斯卡尔算法

/*
 * 克鲁斯卡尔(Kruskal)最小生成树
 */
public void kruskal() {
    int index = 0;                      // rets数组的索引
    int[] vends = new int[mEdgNum];     // 用于保存"已有最小生成树"中每个顶点在该最小树中的终点。
    EData[] rets = new EData[mEdgNum];  // 结果数组,保存kruskal最小生成树的边
    EData[] edges;                      // 图对应的所有边

    // 获取"图中所有的边"
    edges = getEdges();
    // 将边按照"权"的大小进行排序(从小到大)
    sortEdges(edges, mEdgNum);

    for (int i=0; i<mEdgNum; i++) {
        int p1 = getPosition(edges[i].start);      // 获取第i条边的"起点"的序号
        int p2 = getPosition(edges[i].end);        // 获取第i条边的"终点"的序号

        int m = getEnd(vends, p1);                 // 获取p1在"已有的最小生成树"中的终点
        int n = getEnd(vends, p2);                 // 获取p2在"已有的最小生成树"中的终点
        // 如果m!=n,意味着"边i"与"已经添加到最小生成树中的顶点"没有形成环路
        if (m != n) {
            vends[m] = n;                       // 设置m在"已有的最小生成树"中的终点为n
            rets[index++] = edges[i];           // 保存结果
        }
    }

    // 统计并打印"kruskal最小生成树"的信息
    int length = 0;
    for (int i = 0; i < index; i++)
        length += rets[i].weight;
    System.out.printf("Kruskal=%d: ", length);
    for (int i = 0; i < index; i++)
        System.out.printf("(%c,%c) ", rets[i].start, rets[i].end);
    System.out.printf("\n");
}

6、普里姆(Prim)算法,是用来求加权连通图的最小生成树的算法。

–基本思想:对于图G而言,V是所有顶点的集合;
设置两个新的集合U和T,
U用于存放G的最小生成树中的顶点,
T存放G的最小生成树中的边。

从所有uЄU,(V-U表示出去U的所有顶点)
vЄ(V-U)的边中选取权值最小的边(u, v),
将顶点v加入集合U中,
将边(u, v)加入集合T中,
如此不断重复,直到U=V为止,
最小生成树构造完毕,这时集合T中包含了最小生成树中的所有边。

/*
 * prim最小生成树
 *
 * 参数说明:
 *   start -- 从图中的第start个元素开始,生成最小树
 */
public void prim(int start) {
    int num = mVexs.length;         // 顶点个数
    int index=0;                    // prim最小树的索引,即prims数组的索引
    char[] prims  = new char[num];  // prim最小树的结果数组
    int[] weights = new int[num];   // 顶点间边的权值

    // prim最小生成树中第一个数是"图中第start个顶点",因为是从start开始的。
    prims[index++] = mVexs[start];

    // 初始化"顶点的权值数组",
    // 将每个顶点的权值初始化为"第start个顶点"到"该顶点"的权值。
    for (int i = 0; i < num; i++ )
        weights[i] = mMatrix[start][i];
    // 将第start个顶点的权值初始化为0。
    // 可以理解为"第start个顶点到它自身的距离为0"。
    weights[start] = 0;

    for (int i = 0; i < num; i++) {
        // 由于从start开始的,因此不需要再对第start个顶点进行处理。
        if(start == i)
            continue;

        int j = 0;
        int k = 0;
        int min = INF;
        // 在未被加入到最小生成树的顶点中,找出权值最小的顶点。
        while (j < num) {
            // 若weights[j]=0,意味着"第j个节点已经被排序过"(或者说已经加入了最小生成树中)。
            if (weights[j] != 0 && weights[j] < min) {
                min = weights[j];
                k = j;
            }
            j++;
        }

        // 经过上面的处理后,在未被加入到最小生成树的顶点中,权值最小的顶点是第k个顶点。
        // 将第k个顶点加入到最小生成树的结果数组中
        prims[index++] = mVexs[k];
        // 将"第k个顶点的权值"标记为0,意味着第k个顶点已经排序过了(或者说已经加入了最小树结果中)。
        weights[k] = 0;
        // 当第k个顶点被加入到最小生成树的结果数组中之后,更新其它顶点的权值。
        for (j = 0 ; j < num; j++) {
            // 当第j个节点没有被处理,并且需要更新时才被更新。
            if (weights[j] != 0 && mMatrix[k][j] < weights[j])
                weights[j] = mMatrix[k][j];
        }
    }

    // 计算最小生成树的权值
    int sum = 0;
    for (int i = 1; i < index; i++) {
        int min = INF;
        // 获取prims[i]在mMatrix中的位置
        int n = getPosition(prims[i]);
        // 在vexs[0...i]中,找出到j的权值最小的顶点。
        for (int j = 0; j < i; j++) {
            int m = getPosition(prims[j]);
            if (mMatrix[m][n]<min)
                min = mMatrix[m][n];
        }
        sum += min;
    }
    // 打印最小生成树
    System.out.printf("PRIM(%c)=%d: ", mVexs[start], sum);
    for (int i = 0; i < index; i++)
        System.out.printf("%c ", prims[i]);
    System.out.printf("\n");
}

7、迪杰斯特拉(Dijkstra)算法是典型最短路径算法

–用于计算一个节点到其他节点的最短路径。
----主要特点:以起始点为中心向外层层扩展(广度优先搜索思想),直到扩展到终点为止。
----基本思想:通过Dijkstra计算图G中的最短路径时,
需要指定起点s(即从顶点s开始计算)。
–此外,引进两个集合S和U。
S的作用是记录已求出最短路径的顶点(以及相应的最短路径长度),
U则是记录还未求出最短路径的顶点(以及该顶点到起点s的距离)。
–初始时,S中只有起点s;
U中是除s之外的顶点,
并且U中顶点的路径是"起点s到该顶点的路径"。
–然后,从U中找出路径最短的顶点,并将其加入到S中;
接着,更新U中的顶点和顶点对应的路径。
然后,再从U中找出路径最短的顶点,并将其加入到S中;
接着,更新U中的顶点和顶点对应的路径。 …
重复该操作,直到遍历完所有顶点。

----操作步骤

  1. 初始时,S只包含起点s;
    U包含除s外的其他顶点,
    且U中顶点的距离为"起点s到该顶点的距离"
    例如,U中顶点v的距离为(s,v)的长度,然后s和v不相邻,则v的距离为∞。
  2. 从U中选出"距离最短的顶点k",
    并将顶点k加入到S中;
    同时,从U中移除顶点k。
  3. 更新U中各个顶点到起点s的距离。
    之所以更新U中顶点的距离,
    是由于上一步中确定了k是求出最短路径的顶点,
    从而可以利用k来更新其它顶点的距离;
    例如,(s,v)的距离可能大于(s,k)+(k,v)的距离。
  4. 重复步骤2)和3),直到遍历完所有顶点。
/*
 * Dijkstra最短路径。
 * 即,统计图中"顶点vs"到其它各个顶点的最短路径。
 *
 * 参数说明:
 *       vs -- 起始顶点(start vertex)。即计算"顶点vs"到其它顶点的最短路径。
 *     prev -- 前驱顶点数组。即,prev[i]的值是"顶点vs"到"顶点i"的最短路径所经历的全部顶点中,位于"顶点i"之前的那个顶点。
 *     dist -- 长度数组。即,dist[i]是"顶点vs"到"顶点i"的最短路径的长度。
 */
public void dijkstra(int vs, int[] prev, int[] dist) {
    // flag[i]=true表示"顶点vs"到"顶点i"的最短路径已成功获取
    boolean[] flag = new boolean[mVexs.length];

    // 初始化
    for (int i = 0; i < mVexs.length; i++) {
        flag[i] = false;          // 顶点i的最短路径还没获取到。
        prev[i] = 0;              // 顶点i的前驱顶点为0。
        dist[i] = mMatrix[vs][i];  // 顶点i的最短路径为"顶点vs"到"顶点i"的权。
    }

    // 对"顶点vs"自身进行初始化
    flag[vs] = true;
    dist[vs] = 0;

    // 遍历mVexs.length-1次;每次找出一个顶点的最短路径。
    int k=0;
    for (int i = 1; i < mVexs.length; i++) {
        // 寻找当前最小的路径;
        // 即,在未获取最短路径的顶点中,找到离vs最近的顶点(k)。
        int min = INF;
        for (int j = 0; j < mVexs.length; j++) {
            if (flag[j]==false && dist[j]<min) {
                min = dist[j];
                k = j;
            }
        }
        // 标记"顶点k"为已经获取到最短路径
        flag[k] = true;

        // 修正当前最短路径和前驱顶点
        // 即,当已经"顶点k的最短路径"之后,更新"未获取最短路径的顶点的最短路径和前驱顶点"。
        for (int j = 0; j < mVexs.length; j++) {
            int tmp = (mMatrix[k][j]==INF ? INF : (min + mMatrix[k][j]));
            if (flag[j]==false && (tmp<dist[j]) ) {
                dist[j] = tmp;
                prev[j] = k;
            }
        }
    }

    // 打印dijkstra最短路径的结果
    System.out.printf("dijkstra(%c): \n", mVexs[vs]);
    for (int i=0; i < mVexs.length; i++)
        System.out.printf("  shortest(%c, %c)=%d\n", mVexs[vs], mVexs[i], dist[i]);
}

8、弗洛伊德(Floyd)算法是一种用于寻找给定的加权图中顶点间最短路径的算法。

–通过Floyd计算图G=(V,E)中各个顶点的最短路径时,
需要引入一个矩阵S,
矩阵S中的元素a[i][j]表示顶点i(第i个顶点)到顶点j(第j个顶点)的距离。

​----假设图G中顶点个数为N,则需要对矩阵S进行N次更新。
初始时,矩阵S中顶点a[i][j]的距离为顶点i到顶点j的权值;
如果i和j不相邻,则a[i][j]=∞。
接下来开始,对矩阵S进行N次更新。
第1次更新时,如果"a[i][j]的距离" > “a[i][0]+a[0][j]”
(a[i][0]+a[0][j]表示"i与j之间经过第1个顶点的距离"),
则更新a[i][j]为"a[i][0]+a[0][j]"。
同理,第k次更新时,如果"a[i][j]的距离" > “a[i][k]+a[k][j]”,
则更新a[i][j]为"a[i][k]+a[k][j]"。
更新N次之后,操作完成!

/*
 * floyd最短路径。
 * 即,统计图中各个顶点间的最短路径。
 *
 * 参数说明:
 *     path -- 路径。path[i][j]=k表示,"顶点i"到"顶点j"的最短路径会经过顶点k。
 *     dist -- 长度数组。即,dist[i][j]=sum表示,"顶点i"到"顶点j"的最短路径的长度是sum。
 */
public void floyd(int[][] path, int[][] dist) {

    // 初始化
    for (int i = 0; i < mVexs.length; i++) {
        for (int j = 0; j < mVexs.length; j++) {
            dist[i][j] = mMatrix[i][j];    // "顶点i"到"顶点j"的路径长度为"i到j的权值"。
            path[i][j] = j;                // "顶点i"到"顶点j"的最短路径是经过顶点j。
        }
    }

    // 计算最短路径
    for (int k = 0; k < mVexs.length; k++) {
        for (int i = 0; i < mVexs.length; i++) {
            for (int j = 0; j < mVexs.length; j++) {

                // 如果经过下标为k顶点路径比原两点间路径更短,则更新dist[i][j]和path[i][j]
                int tmp = (dist[i][k]==INF || dist[k][j]==INF) ? INF : (dist[i][k] + dist[k][j]);
                if (dist[i][j] > tmp) {
                    // "i到j最短路径"对应的值设,为更小的一个(即经过k)
                    dist[i][j] = tmp;
                    // "i到j最短路径"对应的路径,经过k
                    path[i][j] = path[i][k];
                }
            }
        }
    }

    // 打印floyd最短路径的结果
    System.out.printf("floyd: \n");
    for (int i = 0; i < mVexs.length; i++) {
        for (int j = 0; j < mVexs.length; j++)
            System.out.printf("%2d  ", dist[i][j]);
        System.out.printf("\n");
    }
}

9、哈希表就是一种以 键-值(key-indexed) 存储数据的结构

1.只要输入待查找的值即key,即可查找到其对应的值。
----如果所有的键都是整数,那么就可以使用一个简单的无序数组来实现:
将键作为索引,值即为其对应的值,
这样就可以快速访问任意键的值。
这是对于简单的键的情况,我们将其扩展到可以处理更加复杂的类型的键。

2.使用哈希查找有两个步骤:
使用哈希函数将被查找的键转换为数组的索引。
在理想的情况下,不同的键会被转换为不同的索引值,
但是在有些情况下我们需要处理多个键被哈希到同一个索引值的情况。
所以哈希查找的第二个步骤就是处理冲突

3.哈希表是一个在时间和空间上做出权衡的经典例子。
–如果没有内存限制,那么可以直接将键作为数组的索引。
那么所有的查找时间复杂度为O(1);
–如果没有时间限制,那么我们可以使用无序数组并进行顺序查找,
这样只需要很少的内存。
–哈希表使用了适度的时间和空间来在这两个极端之间找到了平衡。
只需要调整哈希函数算法即可在时间和空间上做出取舍。

4.哈希查找第一步就是使用哈希函数将键映射成索引。
这种映射函数就是哈希函数。
–如果我们有一个保存0-M数组,
那么我们就需要一个能够将任意键转换为该数组范围内的索引(0~M-1)的哈希函数。
–哈希函数需要易于计算并且能够均匀分布所有键。

–比如,使用手机号码后三位就比前三位作为key更好,因为前三位手机号码的重复率很高。
再比如使用身份证号码出生年月位数要比使用前几位数要更好。

–在实际中,我们的键并不都是数字,有可能是字符串,
还有可能是几个值的组合等,所以我们需要实现自己的哈希函数。

5.正整数
获取正整数哈希值最常用的方法是使用除留余数法。
即对于大小为素数M的数组,对于任意正整数k,
计算k除以M的余数。
M一般取素数。

6.字符串
将字符串作为键的时候,我们也可以将他作为一个大的整数,
采用保留除余法。
我们可以将组成字符串的每一个字符取值然后进行哈希
在这里插入图片描述
–举个例子,比如要获取”call”的哈希值,
字符串c对应的unicode为99,a对应的unicode为97,L对应的unicode为108,
所以字符串”call”的哈希值为
在这里插入图片描述
7.避免哈希冲突
1)拉链法
通过哈希函数,我们可以将键转换为数组的索引(0-M-1),
但是对于两个或者多个键具有相同索引值的情况,我们需要有一种方法来处理这种冲突。
–一种比较直接的办法就是,
将大小为M 的数组的每一个元素指向一个条链表,
链表中的每一个节点都存储散列值为该索引的键值对,这就是拉链法。

2)线性探测法是开放寻址法解决哈希冲突的一种方法,
–基本原理为,使用大小为M的数组来保存N个键值对,
其中M>N,我们需要使用数组中的空位解决碰撞冲突。

10、排序算法

1.冒泡排序
基本思想:比较相邻的元素。
如果第一个比第二个大,就交换他们两个。
----时间复杂度O(n*n)

public static void bubbleSort(int[] numbers) {
    int temp = 0;
    int size = numbers.length;
    boolean flag = true;
    for (int i = 0; i < size - 1&&flag; i++) {
        flag = false;
        for (int j = 0; j < size - 1 - i; j++) {
            if (numbers[j] > numbers[j + 1]) // 交换两数位置
            {
                temp = numbers[j];
                numbers[j] = numbers[j + 1];
                numbers[j + 1] = temp;
                flag = true;
            }
        }
    }
}

2.选择排序算法
基本思想:在要排序的一组数中,
选出最小的一个数与第一个位置的数交换;
然后在剩下的数当中再找最小的与第二个位置的数交换,
如此循环到倒数第二个数和最后一个数比较为止。
----时间复杂度O(n*n)
性能上优于冒泡排序,交换次数少

public static void selectSort(int[] numbers) {
    int size = numbers.length; // 数组长度
    int temp = 0; // 中间变量
    for (int i = 0; i < size-1; i++) {
        int k = i; // 待确定的位置
        // 选择出应该在第i个位置的数
        for (int j = size - 1; j > i; j--) {
            if (numbers[j] < numbers[k]) {
                k = j;
            }
        }
        // 交换两个数
        temp = numbers[i];
        numbers[i] = numbers[k];
        numbers[k] = temp;
    }
}

3.插入排序算法
基本思想:每步将一个待排序的记录,
按其顺序码大小插入到前面已经排序的字序列的合适位置
(从后向前找到合适位置后),直到全部插入排序完为止。
----时间复杂度O(n*n)
性能上优于冒泡排序和选择排序

public static void insertSort(int[] numbers) {
    int size = numbers.length;
    int temp = 0;
    int j = 0;
    for (int i = 1; i < size; i++) {
        temp = numbers[i];
        // 假如temp比前面的值小,则将前面的值后移
        for (j = i; j > 0 && temp < numbers[j - 1]; j--) {
            numbers[j] = numbers[j - 1];
        }
        numbers[j] = temp;
    }
}

4.希尔排序算法
基本思想:先将整个待排序的记录序列
分割成为若干子序列分别进行直接插入排序,
待整个序列中的记录“基本有序”时,
再对全体记录进行依次直接插入排序。
----时间复杂度O(n^1.5)

/**
 * 希尔排序的原理:根据需求,如果你想要结果从小到大排列,它会首先将数组进行分组,然后将较小值移到前面,较大值
 * 移到后面,最后将整个数组进行插入排序,这样比起一开始就用插入排序减少了数据交换和移动的次数,
 * 可以说希尔排序是加强 版的插入排序 拿数组5, 2,8, 9, 1, 3,4来说,数组长度为7,当increment为3时,数组分为两个序列
 * 5,2,8和9,1,3,4,第一次排序,9和5比较,1和2比较,3和8比较,4和比其下标值小increment的数组值相比较
 * 此例子是按照从小到大排列,所以小的会排在前面,第一次排序后数组为5, 1, 3, 4, 2, 8,9
 * 第一次后increment的值变为3/2=1,此时对数组进行插入排序, 实现数组从大到小排
 */
public static void shellSort(int[] data) {
    int j = 0;
    int temp = 0;
    // 每次将步长缩短为原来的一半
    for (int increment = data.length / 2; increment > 0; increment /= 2) {
        for (int i = increment; i < data.length; i++) {
            temp = data[i];
            for (j = i; j >= increment; j -= increment) {
                if (temp < data[j - increment])// 从小到大排
                {
                    data[j] = data[j - increment];
                } else {
                    break;
                }
            }
            data[j] = temp;
        }
    }

5.堆排序算法
----基本思想:堆排序是一种树形选择排序,
是对直接选择排序的有效改进。

----堆的定义:具有n个元素的序列 (h1,h2,…,hn),
当且仅当满足(hi>=h2i,hi>=h2i+1)或(hi<=h2i,hi<=h2i+1)
(i=1,2,…,n/2)时称之为堆。
在这里只讨论满足前者条件的堆。
由堆的定义可以看出,
堆顶元素(即第一个元素)必为最大项(大顶堆)。
完全二叉树可以很直观地表示堆的结构。
堆顶为根,其它为左子树、右子树。

----思想:初始时把要排序的数的序列
看作是一棵顺序存储的二叉树,
调整它们的存储序,使之成为一个 堆,
这时堆的根节点的数最大。
然后将根节点与堆的最后一个节点交换。
然后对前面(n-1)个数重新调整使之成为堆。
依此类推,直到只有两个节点的堆,
并对它们作交换,最后得到有n个节点的有序序列。
–从算法描述来看,堆排序需要两个过程,
一是建立堆,二是堆顶与堆的最后一个元素交换位置。
所以堆排序有两个函数组成。
一是建堆的渗透函数,二是反复调用渗透函数实现排序的函数。

----时间复杂度O(nlogn)不适合待排序序列较少的情况

6.快速排序算法
基本思想:
通过一趟排序将待排序记录分割成独立的两部分,其中一部分记录的关键字均比另一部分关键字小,则分别对这两部分继续进行排序,直到整个序列有序。
----时间复杂度O(nlogn)
快速排序在序列中元素很少时,
效率将比较低,不如插入排序,
因此一般在序列中元素很少时使用插入排序,这样可以提高整体效率。

/**
 * 快速排序
 * 
 * @param numbers
 *            带排序数组
 */
public static void quick(int[] numbers) {
    if (numbers.length > 0) // 查看数组是否为空
    {
        quickSort(numbers, 0, numbers.length - 1);
    }
}
/**
 * 
 * @param numbers
 *            带排序数组
 * @param low
 *            开始位置
 * @param high
 *            结束位置
 */
public static void quickSort(int[] numbers, int low, int high) {
    if (low >= high) {
        return;
    }
    int middle = getMiddle(numbers, low, high); // 将numbers数组进行一分为二
    quickSort(numbers, low, middle - 1); // 对低字段表进行递归排序
    quickSort(numbers, middle + 1, high); // 对高字段表进行递归排序
}
/**
 * 查找出中轴(默认是最低位low)的在numbers数组排序后所在位置
 * 
 * @param numbers
 *            带查找数组
 * @param low
 *            开始位置
 * @param high
 *            结束位置
 * @return 中轴所在位置
 */
public static int getMiddle(int[] numbers, int low, int high) {
    int temp = numbers[low]; // 数组的第一个作为中轴
    while (low < high) {
        while (low < high && numbers[high] > temp) {
            high--;
        }
        numbers[low] = numbers[high];// 比中轴小的记录移到低端
        while (low < high && numbers[low] < temp) {
            low++;
        }
        numbers[high] = numbers[low]; // 比中轴大的记录移到高端
    }
    numbers[low] = temp; // 中轴记录到尾
    return low; // 返回中轴的位置
}

7.归并排序算法
基本思想:
归并(Merge)排序法是将两个(或两个以上)有序表
合并成一个新的有序表,
即把待排序序列分为若干个子序列,
每个子序列是有序的。
然后再把有序子序列合并为整体有序序列。
----时间复杂度O(nlogn)

/**
 * 归并排序
 * 简介:将两个(或两个以上)有序表合并成一个新的有序表 即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列
 * 时间复杂度为O(nlogn)
 * 稳定排序方式
 * @param nums 待排序数组
 * @return 输出有序数组
 */
public static int[] sort(int[] nums, int low, int high) {
    int mid = (low + high) / 2;
    if (low < high) {
        // 左边
        sort(nums, low, mid);
        // 右边
        sort(nums, mid + 1, high);
        // 左右归并
        merge(nums, low, mid, high);
    }
    return nums;
}
/**
 * 将数组中low到high位置的数进行排序
 * @param nums 待排序数组
 * @param low 待排的开始位置
 * @param mid 待排中间位置
 * @param high 待排结束位置
 */
public static void merge(int[] nums, int low, int mid, int high) {
    int[] temp = new int[high - low + 1];
    int i = low;// 左指针
    int j = mid + 1;// 右指针
    int k = 0;
    // 把较小的数先移到新数组中
    while (i <= mid && j <= high) {
        if (nums[i] < nums[j]) {
            temp[k++] = nums[i++];
        } else {
            temp[k++] = nums[j++];
        }
    }
    // 把左边剩余的数移入数组
    while (i <= mid) {
        temp[k++] = nums[i++];
    }
    // 把右边边剩余的数移入数组
    while (j <= high) {
        temp[k++] = nums[j++];
    }
    // 把新数组中的数覆盖nums数组
    for (int k2 = 0; k2 < temp.length; k2++) {
        nums[k2 + low] = temp[k2];
    }
}

11、数据结构面试问题解决

1、海量日志数据,提取出某日访问百度次数最多的那个IP。
----算法思想:分而治之+Hash
1).IP地址最多有2^32=4G种取值情况,所以不能完全加载到内存中处理;
2).可以考虑采用“分而治之”的思想,
按照IP地址的Hash(IP)%1024值,
把海量IP日志分别存储到1024个小文件中。
这样,每个小文件最多包含4MB个IP地址;
3).对于每一个小文件,可以构建一个IP为key,
出现次数为value的Hash map,
同时记录当前出现次数最多的那个IP地址;
4).可以得到1024个小文件中的出现次数最多的IP,
再依据常规的排序算法得到总体上出现次数最多的IP;

2、 搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节。假设目前有一千万个记录(这些查询串的重复度比较高,虽然总数是1千万,但如果除去重复后,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就是越热门。)
统计最热门的10个查询串,要求使用的内存不能超过1G。
可以在内存中处理,典型的Top K算法
----算法思想:hashmap+堆
1).先对这批海量数据预处理,在O(N)的时间内用Hash表完成统计;
2).借助堆这个数据结构,找出Top K,时间复杂度为O(N*logK)。
或者:采用trie树,关键字域存该查询串出现的次数,没有出现为0;
最后用10个元素的最小推来对出现频率进行排序。

3、有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词。
----算法思想:分而治之 + hash统计 + 堆排序
1).顺序读文件中,对于每个词x,取hash(x)%5000,
然后按照该值存到5000个小文件(记为x0,x1,…x4999)中。
这样每个文件大概是200k左右。
如果其中的有的文件超过了1M大小,还可以按照类似的方法继续往下分,
直到分解得到的小文件的大小都不超过1M。
2).对每个小文件,采用trie树/hash_map等统计每个文件中出现的词以及相应的频率。
3).取出出现频率最大的100个词(可以用含100个结点的最小堆)后,
再把100个词及相应的频率存入文件,
这样又得到了5000个文件。
最后就是把这5000个文件进行归并(类似于归并排序)的过程了。

4、有10个文件,每个文件1G,每个文件的每一行存放的都是用户的query,每个文件的query都可能重复。要求按照query的频度排序。
----方案1:
算法思想:分而治之 + hash统计 + 堆排序
1)顺序读取10个文件,按照hash(query)%10的结果
将query写入到另外10个文件中。
这样新生成的文件每个的大小大约也1G,
大于1G继续按照上述思路分。
2)找一台内存在2G左右的机器,
依次对用hash_map(query, query_count)来统计每个query出现的次数。
利用快速/堆/归并排序按照出现次数进行排序。
将排序好的query和对应的query_cout输出到文件中。
这样得到了10个排好序的文件(记为)。
3)对这10个文件进行归并排序(内排序与外排序相结合)。
----方案2:
算法思想:hashmap+堆
1)一般query的总量是有限的,只是重复的次数比较多而已,
可能对于所有的query,一次性就可以加入到内存了。
2)采用trie树/hash_map等直接来统计每个query出现的次数,
然后按出现次数做快速/堆/归并排序就可以了。

5、 给定a、b两个文件,各存放50亿个url,每个url各占64字节,内存限制是4G,找出a、b文件共同的url
----方案1:可以估计每个文件安的大小为5G×64=320G,远远大于内存限制的4G。
所以不可能将其完全加载到内存中处理。考虑采取分而治之的方法。
算法思想:分而治之 + hash统计
1)遍历文件a,对每个url求取hash(url)%1000,
然后根据所取得的值将url分别存储到1000个小文件(记为a0,a1,…,a999)中。
这样每个小文件的大约为300M。
2)遍历文件b,采取和a相同的方式将url分别存储到1000小文件(记为b0,b1,…,b999)。
这样处理后,所有可能相同的url都在对应的小文件(a0vsb0,a1vsb1,…,a999vsb999)中,
不对应的小文件不可能有相同的url。
然后我们只要求出1000对小文件中相同的url即可。
3)求每对小文件中相同的url时,
可以把其中一个小文件的url存储到hash_set中。
然后遍历另一个小文件的每个url,
看其是否在刚才构建的hash_set中,
如果是,那么就是共同的url,存到文件里面就可以了。
----方案2:如果允许有一定的错误率,可以使用Bloom filter,4G内存大概可以表示340亿bit。
将其中一个文件中的url使用Bloom filter映射为这340亿bit,
然后挨个读取另外一个文件的url,检查是否与Bloom filter,
如果是,那么该url应该是共同的url(注意会有一定的错误率)。

6、在2.5亿个整数中找出不重复的整数,注,内存不足以容纳这2.5亿个整数。
----采用2-Bitmap(每个数分配2bit,
00表示不存在,01表示出现一次,10表示多次,11无意义)进行,
共需内存2^32 * 2 bit=1 GB内存,还可以接受。
然后扫描这2.5亿个整数,查看Bitmap中相对应位,
如果是00变01,01变10,10保持不变。
所描完事后,查看bitmap,把对应位是01的整数输出即可。

7、给40亿个不重复的unsigned int的整数,没排过序的,然后再给一个数,如何快速判断这个数是否在那40亿个数当中?
----方案1:申请512M的内存,一个bit位代表一个unsigned int值。
读入40亿个数,设置相应的bit位,
读入要查询的数,查看相应bit位是否为1,
为1表示存在,为0表示不存在。
----方案2:因为2^32为40亿多,所以给定一个数可能在,也可能不在其中;
这里我们把40亿个数中的每一个用32位的二进制来表示:
假设这40亿个数开始放在一个文件中。
1)然后将这40亿个数分成两类:最高位为0和最高位为1
并将这两类分别写入到两个文件中,
其中一个文件中数的个数<=20亿,
而另一个>=20亿(这相当于折半了);
与要查找的数的最高位比较并接着进入相应的文件再查找
2)再然后把这个文件为又分成两类:次最高位为0和次最高位为1
并将这两类分别写入到两个文件中,
其中一个文件中数的个数<=10亿,
而另一个>=10亿(这相当于折半了);
与要查找的数的次最高位比较并接着进入相应的文件再查找。

3)以此类推,就可以找到了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值