【算法】常见数据结构基本算法整理


去年11月份听了牛客网的课,当时做了纸质的笔记整理。

现在为了以后方便查找,将问题目录整理如下。

每道题只提供解题思路,不贴源代码。

可能会稍微手写一下代码(没有在IDE上测,为了试下以后面试时手写代码),或者伪代码。

by 03/17/2018

其实本科学过数据结构、算法设计,而且后来也看过不止一次,但这次又听左神讲,真的是“每次都有新发现”,觉得很有趣,必须要记录一下。

该篇博客中某些算法的具体讲解可以去博客:
http://blog.csdn.net/liuxiao214/article/details/78649765 中查看。


小目录

虽然博有自带目录,我还是想把所有内容都列出来。

1、时间复杂度计算

2、冒泡排序、利用位运算交换元素

2.1 利用位运算交换

2.2 冒泡排序

3、插入排序

4、选择排序

5、随机快排

6、二分查找

7、Master公式

8、归并排序

9、利用归并排序求小和

10、利用归并排序求逆序对

11、堆排序建堆调整 堆

12、桶排序

13、比较器的使用

14、计数排序

15、排序后最大相邻数差值问题

16、KMP算法

17、KMP算法应用最大重合的新串

18、KMP算法应用两个二叉树是否子树匹配

19、Manacher算法及其扩展添加新串使回文且最短

19.1 Manacher找出字符串str最大的回文子串

19.2 给定str1在其后添加字符使得新字符串是回文字符串且最短

20、BFPRT算法求第K小大的数

21、基数排序

22、希尔排序

23、实现特殊栈,O(1)实现getmin()

24、用数组结构实现大小固定的队列和栈

25、用栈实现队列、用队列实现栈

26、猫狗队列

27、哈希表和哈希函数

28、设计RandomPool结构

29、转圈打印矩阵、转90度打印矩阵

30、之字型打印矩阵

31、在行列有序的矩阵中找数

32、打印两个有序链表的公共部分

33、判断一个链表是否是回文结构

34、将单向链表按照某值划分为左边小、中间相等、右边大结构,且内部保持原有顺序

35、复制含有随机指针节点的链表

36、两个单链表相交的一系列问题

37、反转单向、双向链表

38、随时找到数据流的中位数

39、金条切块

40、求收益问题

41、折纸问题

42、二叉树的前序、中序、后序遍历的递归与非递归实现

43、较为直观的打印二叉树

44、在二叉树中找到一个节点的后继节点

45、并查集

46、布隆过滤器

47、二分搜索

48、前缀树及其六个应用

48.1 找B数组中哪些串是A数组中串的前缀

48.2 查找某个字符串是否在数组中

48.3 查找某个字符串出现否,且出现几次

48.4 二叉树曾经有多少个路径经过当前节点

48.5 删除字符串

48.6 整型数组中都是3位数,是否能得到最趋近999的和

49、图、无向图、有向图、邻接表、邻接矩阵

50、图的宽度优先遍历

51、图的深度优先遍历

52、拓扑排序

53、最小生成树:Kruskal算法、Prim算法

54、Dijkstra算法

55、递归概念

56、汉诺塔问题

57、打印一个字符串的全部子序列

58、打印一个字符串的全部序列

59、母牛生小牛问题、人走台阶问题

60、给定数字字符串,转换成字母字符串

61、逆序栈,不用额外空间,递归

62、动态规划概念

63、数组从左上走到右下的最小路径和

64、数组中的数是否能累加到aim

65、01背包问题

66、堆的应用、攻克城市

67、数组代表容器,求max装水量

68、最大的leftmax与rightmax之差的绝对值

69、求最大子数组和

70、字符串是否可循环右移得到某字符串

71、字符串右移K位后的字符串,原地调整

72、生成窗口最大值数组

73、拼接字符串,使其字典顺序最小

74、占用会议室问题

75、Morris遍历

76、搜索二叉树

77、平衡树AVL

78、SB树、红黑树

79、跳表


一、第一次课(2017.11.11)

1、时间复杂度计算

描述一个流程中常数操作数的指标。只看高阶项,忽略常数系数。

2、冒泡排序、利用位运算交换元素

2.1 利用位运算交换

已知:a^a=0 a^0=a

void swap(int &a ,int &b)
{
a = a^b;  //
b = a^b;  //b=a^b^b=a
a = a^b:  //a=a^b^a=b
}

注意,这种操作要保证两个数是不同地址的数,否则会发生自己跟自己异或结果为0的情况。

2.2 冒泡排序

每次都首项开始依次向后比较,将大的数向后传送,找到最大的那个,放到末尾。第二次时找到倒数第二大的数。

时间:O(N^2)
空间:O(1)
可做到稳定性

void bubble_sort(vector<int>&a)
{
    for(int i=a.size()-1;i>0;i--)
    {
        for(int j=0;j<i;j++)
        {
            if(a[j+1]>a[j])
                swap(a[j+1],a[j]);
        }
    }
}

3、插入排序

类似摸牌,从数组第二个位置开始,依次向前面已经排好序的数组中插入。

时间:O(N^2)
空间:O(1)
可做到稳定性

void insert_sort(vector<int>&a)
{
    for(int i=1;i<a.size();i++)
    {
        for(int j=i;j>0;j--)
        {
            if(a[j]<a[j-1])
                swap(a[j],a[j-1]);
        }
    }
}

4、选择排序

从第一个位置开始,从后开始找到最小的那个,放在第一个位置;
继续从第二个位置往后找第二小,放第二个位置;
…….

时间:O(N^2)
空间:O(1)
不可做到稳定性

void select_sort(vector<int>&a)
{
    for(int i=0;i<a.size()-1;i++)
    {
        int min = i;
        for(int j=i+1;j<a.size();j++)
        {
            if(a[j]<a[min])
                min=j;
        }
        swap(a[i],a[min]);
    }
}

5、随机快排

时间:O(NlogN)
空间:O(logN)
常规实现做不到稳定性,有论文实现,不必考虑

利用递归,每次随机选取一个值,将其作为基准值,小于它的放左边,等于的放中间,大于的放右边,然后递归调用左右两边。

vector<int> partition(vector<int>a, int l, int r)
{
    int less = l-1;
    int more = r;
    while(l<more)
    {
        if(a[l]<a[r])
            swap(a[++less], a[l++]);
        else if(a[l]>a[r])
            swap(a[--more], a[l]);
        else
            l++;
    }
    swap(a[r],a[more]);
    vector<int>p;
    p.push_back(less+1);
    p.push_back(more);
    return p;
}

void quick(vector<int>a, int l, int r)
{
    if(l>=r)
        return;
    int pindex = rand()%(r-l+1);
    swap(a[l + pindex],a[r]);
    vector<int>p = partition(a, l, r);
    quick(a, l, a[p[0]-1]);
    quick(a, a[p[1]+1], r);
}

void quick_sort(vector<int>a)
{
    if (a.size()<2)
        return ;
    quick(a, 0, a.size()-1);
}

6、二分查找

在已经排好序的数组中查找某个数是否存在。一般是取中间值比较,等于则存在,大于则递归调用右边,小于则递归调用左边。

bool binary(vector<int>a, int x)
{
    int l = 0;
    int r = a.size()-1; 
    while(l<r)
    {
        int mid = l + (r-l)>>1;
        if (a[mid]==x)
            return true;
        if (a[mid] > x)
            l = mid + 1;
        if (a[mid] < x)
            r = mid - 1;
    }
    return false;
}

7、Master公式

对于递归行为的复杂度,可以用公式来解决。如果一个N规模的程序可以分为N/b个规模,一共做a次(a次递归,每次N/b次操作),或者还带有其他非递归的复杂度,如下:

T(N)=aT(Nb)+O(Nd)

如果 logba>d ,则 T(N)=O(Nlogba)
如果 logba<d ,则 T(N)=O(Nd)
如果 logba=d ,则 T(N)=O(NdlogN)

二、第二次课(2017.11.12)

8、归并排序

递归划分两组,直到不能再分,组内有序,两两合并排序,需要额外数组。

时间:O(NlogN)
空间:O(N)

void merge(vector<int>&a, int l, int mid, int r)
{
    vector<int>help(r-l+1);
    int p1 = l;
    int p2 = mid + 1;
    int i = 0;
    while(p1<=mid && p2<=r)
        help[i++] = a[p1]<a[p2]?a[p1++]:a[p2++];
    while(p1<=mid)
        help[i++] = a[p1++];
    while(p2<=r)
        help[i++] = a[p2++];
    for(i = 0;i<help.size();i++)
        a[l+i] = help[i];
}
void merge_sort(vector<int>&a, int l, int r)
{
    if(l == r)
        return;
    int mid = l + (r-l)>>1;
    merge_sort(a, l, mid);
    merge_sort(a, mid+1, r);
    merge(a, l, mid, r);
}
merge_sort(a, 0, a.size()-1);

9、利用归并排序求小和

小和就是某个数之前所有比它小的数的和。求小和就是求整个数组的小和之和。

如:3 5 1 4 6,其小和就是3+(3+1)+(3+5+1+4)=20

利用归并排序,每次两两合并时,如果左边组一个数比右边组一个数小,那他肯定比右边组那个数及其后面的数都小。

用O(NlogN)解决。

int merge(vector<int>&a, int l, int mid, int r)
{
    vector<int>help(r-l+1);
    int p1 = l;
    int p2 = mid + 1;
    int i = 0;
    int sum = 0;
    while(p1<=mid && p2<=r)
        sum += a[p1]<a[p2]?(a[p1]*(l-p2+1)):0;
        help[i++] = a[p1]<a[p2]?a[p1++]:a[p2++];
    while(p1<=mid)
        help[i++] = a[p1++];
    while(p2<=r)
        help[i++] = a[p2++];
    for(i = 0;i<help.size();i++)
        a[l+i] = help[i];
    return sum;
}
int merge_sort(vector<int>&a, int l, int r)
{
    if(l == r)
        return;
    int mid = l + (r-l)>>1;
    return merge_sort(a, l, mid)+merge_sort(a, mid+1, r)+merge(a, l, mid, r);
}
merge_sort(a, 0, a.size()-1);

10、利用归并排序求逆序对

逆序对是一个数,前面有比他大的数,这两个数就构成了逆序对。求数组中有多少个逆序对。用O(NlogN)解决。

利用归并排序,当两个组在合并时,如果左边组的一个数比右边组的一个数大,那左边组这个数及其后面的数都会比右边组的这个数大,这些数就构成了逆序对。

int merge(vector<int>&a, int l, int mid, int r)
{
    vector<int>help(r-l+1);
    int p1 = l;
    int p2 = mid + 1;
    int i = 0;
    int sum = 0;
    while(p1<=mid && p2<=r)
        sum += a[p1]>a[p2]?(mid-p1+1):0;
        help[i++] = a[p1]>a[p2]?a[p2++]:a[p1++];
    while(p1<=mid)
        help[i++] = a[p1++];
    while(p2<=r)
        help[i++] = a[p2++];
    for(i = 0;i<help.size();i++)
        a[l+i] = help[i];
    return sum;
}
int merge_sort(vector<int>&a, int l, int r)
{
    if(l == r)
        return;
    int mid = l + (r-l)>>1;
    return merge_sort(a, l, mid)+merge_sort(a, mid+1, r)+merge(a, l, mid, r);
}
merge_sort(a, 0, a.size()-1);

11、堆排序、建堆、调整 堆

算法上的堆是一个完全二叉树,已知节点i,其父节点为(i-1)/2,左孩子为2i+1,右孩子为2i+2。

使用大根堆,任何一个节点,都是其子树中值最大的点。

堆排序,就是把整个数组变成一个大根堆,然后将根节点与尾节点交换,再向下调整大根堆。

如何建立大根堆:将节点依次插入堆中,如果碰到比自己的父节点大,则与其交换,递归向上调整。

根节点与尾节点交换后如何调整大根堆:向下调整,与左右孩子比较,若大于他们,不需要调整,如果小于,则跟其中大的孩子交换。然后递归向下调整。

void heapinsert(vector<int>&a, int index)
{
    while(a[index]>a[(index-1)/2])
    {
        swap(a[index],a[(index-1)/2]);
        index = (index-1)/2;
    }
}
void heapify(vector<int>&a, int index, int size)
{
    int left = 2*index+1;
    while(left<size)
    {
        int largest = ((left+1<size) && (a[left+1]>a[left]))?left+1:left;
        largest = a[largest]>a[index]?largest:index;
        if (largest==index)
            break;
        swap(a[largest], a[index]);
        index = largest;
        left = 2*index+1;   
    }
}
void heap_sort(vector<int>&a)
{
    if(a.size()<2)
        return;
    for(int i=1;i<a.size();i++)
        heapinsert(a, i);
    int size = a.size();
    swap(a[0],a[--size]);
    while(size>0)
    {
        heapify(a, 0, size);
        swap(a[0],a[--size]);
    }
}

12、桶排序

分为计数排序和基数排序。分开说。

这种不基于比较的排序,可实现时间复杂度为O(N),但浪费空间,对数据的位数与范围有限制。

13、比较器的使用

使用系统的默认排序函数sort时,是从小到大的,你可以改变比较器实现不同方式的排序。

bool mycompare1(int a, int b)
{//降序
    return a - b > 0;
}
// 注意不能加等于号!
bool mycompare2(int a, int b)
{//升序
    return b - a > 0;
}
sort(A.begin(), B.end(), mycomapre1);

14、计数排序

根据数组中的最大值max准备max+1个桶,每个桶记录数组中的这个数出现的次数。然后根据桶中的数字一次倒出每个数即可。

void bucket_sort(vector<int>&a)
{
    if(a.size()<2)
        return;
    int max = INT_MIN;
    for (int i=0;i<a.size();i++)
        max = a[i]>max?a[i];max;
    vector<int>bucket(max+1,0);
    for(int i=0;i<a.size();i++)
        bucket[a[i]]++;
    int j=0;
    for(int i=0;i<bucket.size();i++)
    {
        while(bucket[i]-->0)
            a[j++] = i; 
    }
}

15、排序后最大相邻数差值问题

即n个数,则准备n+1个桶,最后一个桶放最大值max,其他min~(max-1)的数在前n个桶桑平分。比如说数i,它放在哪个桶中就取决于:((i-min)/(max-min))*n。

如果说有一个桶为空,则其左边第一个不为空的桶的最大值a,与其右边第一个不为空的桶的最小值b,这两个数在排序后一定是相邻的,且他们的差值比每个桶内的差值都大。所以只需要记录这些每个桶内的max和min,比较相邻的前后非空桶的min-max与桶内的max-min的差值大小即可。

所以准备三个n+1大小的数组,一个数组记录这个桶是否为空,另外两个数组分别记录这个桶内的max和min。

int maxgap(vector<int>a)
{
    if(a.size()<2)
        return 0;
    int imax = INT_MIN;
    int imin = INT_MAX;
    for(int i=0;i<a.size();i++)
    {
        imax = a[i]>imax?a[i]:imax;
        imin = a[i]<imin?a[i]:imin;
    }
    if(imin==imax)
        return 0;
    vector<bool>isempty(a.size()+1);
    vector<int>amax(a.size()+1);
    vector<int>amin(a.size()+1);
    int index=0;
    for(int i=0;i<a.size();i++)
    {
        index = (a[i]-imin)* a.size()/(imax-imin);
        amax[index] = amax[index]?max(amax[index], a[i]):a[i];
        amin[index] = amin[index]?min(amin[index],a[i]):a[i];
        isempty[index] = true;
    }
    int gap = isempty[0]?amax[0]-amin[0]:0;
    int lastmax = amax[0];
    for (int i=1;i<isemppty.size();i++)
    {
        if(isempty[i])
        {
            gap = max(gap, amin[i]-lastmax);
            lastmax = amax[i];
        }   
    }
    return gap;
}

16、KMP算法

想看详细介绍,请参见博客:
http://blog.csdn.net/liuxiao214/article/details/78026473

字符串匹配问题,字符串p是否是字符串s的子串,是的话,返回p在s中的起始位置,否的话返回-1。

暴力的方法是p与s的第一个字符比较,相等就继续比较,不相等就换s的第二个字符继续比较,这样做的话,遍历s的指针i会发生回溯,导致时间复杂度为O(N^2)。

那最好是i指针不用发生回溯。这里就用到了字符串的最大公共前缀后缀。

首先是找字符串p的最大公共前缀后缀,比如ABCAB,其最大公共前缀后缀就是AB,长度是2。这样当我们发生不匹配时,直接按照其最大公共前缀后缀长度移动字符串p就可以。

当然我们不直接使用最大相同公共前缀后缀,我们使用的是next数组,这个数组中的数是不包含当前字符的最大前缀后缀的长度,如图:

这里写图片描述

然后这样字符串匹配时,如果发生不匹配,遍历字符串s的指针i不必回溯,只需移动字符串p即可,即更改遍历p的指针j,j变为其next数组中的值。

求next数组利用递归。直接上代码:

vector<int> getnext(string s)
{
    vector<int>next(s.length());
    next[0] = -1;
    if (s.length()<2)
        return next;
    next[1] = 0;
    int cn = 0;
    int i = 2;
    while(i<s.length())
    {
        if(s[i-1]==s[cn])
            next[i++] = ++cn;
        else if(cn>0)
            cn = next[cn];
        else
            next[i++] = 0;
    }
}
int kmp(string s, string p)
{
    if(s.length()<p.length() || s.length==0 || p.length==0)
        return -1;
    int i = 0;
    int j = 0;
    vector<int>next=getnext(p);
    while(i<s.length() && j<p.length())
    {
        if(s[i]==p[j])
        {
            i++;
            j++;
        }
        else if(next[j]==-1)
            i++;
        else
            j = next[j];
    }
    return j==p.length()?i-j:-1;
}

三、第三次课(2017.11.18)

17、KMP算法应用:最大重合的新串

给定一个字符串str1,只能往str1的后面添加字符变成str2。

要求:str2中必须包含两个str1,即两个str1可以有重合,但不能是同一位置开头,即完全重合;且str2尽量短。

利用next数组,找到str1的最大公共前缀后缀,然后向str1尾部添加从前缀后开始到str1原尾部的字符。

如:str1=abracadabra,则str2=abracadabra+cadabra

int getnext(string s)
{
    vector<int>next(s.length()+1);
    next[0]=-1;
    if (s.length()<2)
        retrun -1;
    next[1] = 0;
    int cn = 0;
    int i = 2;
    while(i<s.length()+1)
    {
        if(s[i-1]==s[cn])
            next[i++] = ++cn;
        else if(cn>0)
            cn = next[cn];
        else
            next[i++] = -1; 
    }
    return next[s.length()];
}
string gettwo(string s)
{
    if (s.length()==0)
        return "";
    if (s.length()==1)
        return s+s;
    if (s.length()==2)
        return s[0]==s[1]?s+s.substr(1):s+s;
    return s + s.substr(getnext(s));
}

18、KMP算法应用:两个二叉树是否子树匹配

给定两个二叉树T1,T2,返回T1的某个子树结构是否与T2相等。

常规解法是遍历头节点,看其子树结构是否与T2相等。

这里将二叉树T1与T2都转换为字符串,是否匹配就看T2是否是T1的子串。

如何转换呢?每一个节点的值后面都加一个特殊符号,比如“_”,空节点用另一个特殊符号表示,如“#”。序列化要按照同样的遍历方式,比如前序遍历。

     3
   1    2
 2   3

上图中序列化后为“3 _ 1 _ 2 _ # _ #_ 3 _ # _ # _ 2 _ # _ # _”

struct TreeNode
{
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode(int x):val(x),left(NULL),right(NULL){}
};

string tree2string(TreeNode *root)
{
    if(root==NULL)
        return "#_";
    string str = to_string(root->val) + "_";
    return str + tree2string(root->left) + tree2string(root->right);
}
vector<int> getnext(string s)
{
    vector<int>next(s.length());
    next[0] = -1;
    if (s.length()<2)
        return s;
    next[1] = 0;
    int cn = 0;
    int i = 2;
    while(i<s.length())
    {
        if(s[i-1]==[cn])
            next[i++] = ++cn;
        else if (cn>0)
            cn = next[cn];
        else
            next[i++] = -1;
    }
    return next;
}
int kmp(string s, string p)
{
    if(s.length() < p.length() || s.length()==0 || p.length()==0)
        return -1;
    vector<int>next = getnext(p);
    int i = 0;
    int j = 0;
    while(i<s.length() && j<p.length())
    {
        if(s[i] == p[j])
        {
            i++;
            j++;
        }
        else if (next[j]>0)
            j = next[j];
        else 
            i++:
    }
    return j == p.length() ? i-j : -1;
}
bool issubtree(TreeNode *root1, TreeNode *root2)
{
    string s = tree2string(root1);
    string p = tree2string(root2);
    return kmp(s, p) != -1;
}

19、Manacher算法及其扩展(添加新串使回文且最短)

19.1 Manacher:找出字符串str最大的回文子串

首先解决奇回文和偶回文的问题。

我们在判断字符串是否回文时,是根据一个字符(奇回文)或空(偶回文)来判断的,这样需要分情况讨论,比较麻烦,所以最好统一一下。

添加辅助字符,(随便哪个字符都可以,不会影响原有字符匹配就可以),如“#”,在字符与字符之间、开头和结尾各添加“#”,这个“#”就相当于一个虚轴,虚轴只会和虚轴匹配。这样就可以统一奇回文和偶回文了。

传统方法是从str的每个字符开始,向两边扩,找到最大回文子串,复杂度为O(N^2)。

首先是将字符串转换为添加特殊字符后的新字符串。然后进行查找最大回文子串。

首先规定几个概念:

  • 回文最右边界R
  • 回文最右边界R的回文中心C
  • 回文半径数组radius,记录每个字符以其为中心的回文半径。
  • 最大回文半径与最大回文半径中心。

一共分为四种情况讨论。

1、当遍历到i时,如果字符i在最右边界R的右边,只能采用暴力匹配方式,向两边扩。
2、如果i在最右边界R的左边,则找到i关于R的对称点i’,如果i’的回文左边界在R的回文边界中,则i的回文边界也在R的回文边界中,不会再向外扩。
3、如果i’的回文左边界不在R的回文边界中,则i的回文右边界刚好与R重合,也不会扩。
4、如果i’的回文左边界刚好在R的回文左边界上,则i的回文半径至少到了R,至于R之后会不会扩,只能暴力匹配了。

那么在上述四种情况下,2、3的回文边界就不用暴力匹配了,直接去radiux[2C-i](即i’的回文半径)与R-i的小值就可以了。

string get_newstring(stirng s)
{
    string ss = "#";
    for (int  i=0;i<s.length();i++)
        ss = ss + s[i] + "#";
    return ss;
}
string manacher(string s)
{
    if(s.length()<2)
        return "";
    string news = get_newstring(s);
    vector<int>radius(news.length(), 0);
    int C = -1;
    int R = -1;
    int rmax = INT_MIN;
    int rc = 0;
    for(int i=0; i<news.length(); i++)
    {
        radius[i] = 1;
        radius[i] = R>i ? min(R-i, radius[2*C-i]) : radius[i];
        while( i - radius[i] >= 0 && i + radius[i] < news.length())
        {
            if(news[i - radius[i]] == news[i + radius[i]])
                radius[i]++;
            else
                break;
        }
        if(i+raidus[i]>R)
        {
            R = i + radius[i];
            C = i;
        }
        if(radius[i]>rmax)
        {
            rmax = radius[i];
            rc = i;
        }
    }
    rmax--;
    return s.substr((ic-imax)/2, imax);
}

19.2 给定str1,在其后添加字符使得新字符串是回文字符串且最短

找到包含最后一个字符的最大回文子串,然后将这个回文子串之前的字符反向贴在str1的后面即可。

如“abc12321”,即将“abc”反向贴在后面即可,“abc12321cba”

string get_newstring(string s)
{
    string news = "#";
    for(int i=0; i<s.length(); i++)
        news = news + s[i] + "#";
    return news;
}
int manacher(string s)
{
    string news = get_newstring(s);
    vector<int>radius(news.length());
    int C = -1;
    int R = -1;
    for(int i=0; i<news.length(); i++)
    {
        radius[i] = R>i ? min(R-i, radius[2*C-i]) : 1;
        while(i - radius[i] >=0 && i + radius[i] < news.length())
        {
            if(news[i-radius[i]] == news[i+radius[i]])
                radius[i]++;
        }
        if(i+radius[i]>R)
        {
            R = i+ radius[i];
            C = i;
            if (R == news.length())
                return radius[i]-1;//包含最后一个字符的最大回文子串的长度
        }
    }   
}
string getmaxstring(string s)
{
    int r = manacher(s);
    string ss = s;
    for(int i= s.length()-r-1; i>=0; i--)
        ss = ss + s[i];
    return ss;
}

20、BFPRT算法:求第K小/大的数

利用快排中的partition过程。如果求第K小,就是求排序后数组中下标为k-1的值。如果k-1属于partition后的等于区,则可直接返回等于区的数,如果,在大于区,则递归调用大于区,在小于区,则递归调用小于区。

到那时BFPRT又做了优化,即在选取partition的划分值时不再是随机选取,而是有策略的选取。

如何选呢?首先,将数组5个一组进行划分,不足5个自动一组。

然后分别组内排序(可用插入排序),求出每个组内的中位数,将这些中位数组成新的数组mediumarray。

然后递归调用这个BFPRT的函数,得到这个中位数数组的上中位数。这个值就是进行partition的划分值。

vector<int> partition(vector<int>&a, int l, int r, int value)
{
    int less = l-1;
    int more = r+1;
    while(l<more)
    {
        if(a[l]<value)
            a[++less]=a[l++};
        else if (a[l]>value)
            a[--more]=a[l];
        else
            l++;
    }
    vector<int>p;
    p.push_back(less+1);
    p.push_back(more-1);
}

void insert_sort(vector<int>&a, int start, int end)
{
    if(start==end)
        return ;
    for(int i=start+1;i<=end;i++)
    {
        for(int j=i-1;j>=start;j--)
        {
            if(a[j+1]<a[j])
                swap(a[j+1],a[j]);
        }
    }
}

int getmedium(vector<int>&a, int start, int end)
{
    insert_sort(a, start, end);
    return a[strat + (end-start)/2];
}

int get_medium_of_medium(vector<int>&a, int start, int end)
{
    int num = end-start+1;
    int flag = num%5 == 0 ? 0 : 1;
    vector<int>mediums(num/5+flag);
    for(int i=0; i< mediums.size(0; i++)
    {
        int istart = start + i*5;
        int iend = istart + 4;
        mediums[i] = getmedium(a, istart, min(iend, end));
    }
    return findk(mediums, 0, mediums.size()-1, (mediums.size()-1)/2);
}

int findk(vector<int>&a, int start, int end, int k)
{
    if(start==end)
        return a[strat];
    int pvalue = get_medium_of_medium(a, start, end);
    vector<int>p = partition(a, start, end, pvalue);
    if(k>=p[0] && k<=p[1])
        return pvalue;
    else if(k<p[0])
        return findk(a, start, p[0]-1, k);
    else
        returun findk(a, p[1]+1, end, k);   
}

int mink(vector<int>a, int k)
{
    if(k<1 || k>a.size())
        return NULL;
    return findk(a, 0, a.size()-1, k-1);
}

21、基数排序

找到最大数的位数,并准备10个桶(0-9)。从个位数到高位数依次排序,按照其位上的数的大小依次入桶,顺序要保持一致,保持先进先出。

int maxbit(vector<int>a)
{
    int imax = INT_MIN;;
    for(int i=0; i<a.size() && imax<a[i]; i++)
        imax = a[i];
    int digit = 0;
    while(imax>0)
    {
        imax /= 10;
        digit++;
    }
    return digit;
}

int getnum(int x, int digit)
{
    return (x/(pow(10, digit-1))) % 10;
}

void radix(vector<int>&a, int start, int end, int digit)
{
    vector<int>help(end-start+1);
    int num;
    for(int d=1; d<=digit; d++)
    {
        vector<int>count(10,0);
        for(int i=0; i<a.size(); i++)
        {
            num = getnum(a[i], d);
            count[num]++;
        }
        fot(int i=1; i<10; i++)
            count[i] += count[i-1];
        for(int i=end; i>=start; i--)
        {
            num = getnum(a[i], d);
            help[count[num]-1] = a[i];
            count[num]--;
        }
        for(int i=0; i<help.size(); i++)
            a[start + i]=help[i];
    }
}

void radix_sort(vector<int>a)
{
    if(a.size()<2)
        return ;
    radix(a, 0 ,a.size()-1, maxbits(a));
}

22、希尔排序

设置步长,每次选一个步长来进行排序,做很多遍,最终落在步长为1的排序上。大步长调整完基本有序,则小步长不必调整过多。

插入排序就是步长为1的希尔排序。

12 3 1 9 4  8 2 6 (未排序)
选择步长为4
4  3 1 6 12 8 2 9
选择步长为2
1 3 2 6 4 8 12 9
选择步长为1
1 2 3 4 6 8 9 12

23、实现特殊栈,O(1)实现getmin()

进栈、出栈、返回栈顶这些都与原来的栈的操作保持一致,主要是加了怎样在O(1)时间下得到栈内最小值。

两种方法,不过都要准备两个栈,一个数据栈data存放数据,一个help栈,存放最小值。

方法一:help栈存放最小值,每次data栈压栈时,help也压栈,不过help要保持最小的栈顶,即如果data新进的数如果比help栈顶小,则这个数可以压栈入help,否则将help栈顶的那个数再一次压入help栈中。

压栈的时候同步压,出栈同步出。

方法二:当压入data栈的数小于等于help栈顶时,可以向help压入,否则不压。当要弹出data栈的数时,如果这个数与help的栈顶值相等,则help也弹出,否则help不弹出。

则help的栈顶一直是当前栈内最小的数。

24、用数组结构实现大小固定的队列和栈

24.1 实现栈

维护一个size(初始为0),代表栈的大小,当push时,数直接放在a[size]上,size++,当pop时,直接取a[size-1]]的值,size–。

struct array_stack
{
    vector<int>astack;
    int size;
    array_stack(int initsize)
    {
        if(initsize<0)
            cout<<"请输入一个大于0的数,以保证栈不为空"<<endl;
        astack=vector<int>(initsize);
        size = 0;
    }
    int top()
    {
        if(size==0)
        {
            cout<<"the stack is empty"<<endl;
            return NULL;
        }
        return a[size-1];
    }
    void push(int x)
    {
        if(size==astack.size())
        {
            cout<<"the stack is full"<<endl;
            return ;
        }
        astack[size++] = x;
    }
    int pop()
    {
        if(size==0)
        {
            cout<<"the stack is empty"<<endl;
            return NULL;
        }
        return astack[--size];
    }
};

24.2 实现队列

维护start、end、size三个指针,size表示队列的大小(约束start与end的关系)。当push时,end++,pop时,start++,用start去追end,谁先触底,则回到开始位置。

struct array_queue
{
    vector<int>aqueue;
    int size;
    int start;
    int end;
    aqueue(int initsize)
    {
        if(initsize<0)
            cout<<"请输入一个大于0的数,以保证队列不为空"<<endl;
        aqueue = vector<int>(initsize);
        size = 0;
        start = 0;
        end = 0;
    }
    int top()
    {
        if(size == 0)
            return null;
        return aqueue[start];
    }
    void push(int x)
    {
        if(size == aqueue.size())
        {
            cout<<"the queue is full"<<endl;
            return ;
        }
        aqueue[end] = x;
        size ++;    
        end = end==aqueu.size()-1?0:end+1;
    }
    int pop()
    {
        if(size == 0)
        {
            cout<<"the queue is empty"<<endl;
            return NULL;
        }
        size--;
        int temp = start;
        start = start == aqueue.size()-1?0:start+1;
        return aqueue[temp];
    }
}

25、用栈实现队列、用队列实现栈

25.1 用栈实现队列

准备两个栈,一个数据栈data,一个辅助栈help,数据栈负责入队列,如果需要出队列,就把数据栈中的当前所有数据倒入辅助栈help中。然后从help中出队列。

注意两点:如果help栈不为空,data栈不要向help栈倒数据;data栈倒数据时,一次要全倒完。

这里写图片描述


struct stack2queue
{
    stack<int>data;
    stack<int>help;
    stack2queue()
    {
        data = new stack<int>();
        help = new stack<int>();
    }
    int top()
    {
        if(data.empty() && help.empty())
        {
            cout<<"the queue is empty"<<endl;
            return NULL;
        }
        else if(help.empty())
        {
            while(!data.empty())
                help.push(data.pop());
        }
        return help.top();
    }
    void push(int x)
    {
        data.push(x);
    }
    int pop()
    {
        if(data.empty() && help.empty())
        {
            cout<<"the queue is empty"<<endl;
            return NULL;
        }
        else if(help.empty())
        {
            while(!data.empty())
                help.push(data.pop());
        }
        return help.pop();
    }
};

25.2 用队列实现栈

准备两个队列A和B,两个队列互相配合,称为数据队列和缓存队列。当进栈时,向数据队列中入,当出栈时,将目前的那个数据栈除最后一个元素外全部出队列,并进入到缓存队列,此时数据队列只剩那个要出栈的元素,可出。

此数据队列变为空,为缓存队列,缓存队列为数据队列。

struct queue2stack
{
    queue<int>data;
    queue<int>help;
    queue2stack()
    {
        data = new queue<int>();
        help = new queue<int>();
    }
    int top()
    {
        if(data.empty())
        {
            cout<<"the stack is empty"<<endl;
            return NULL;
        }
        while(data.size()!=1)
            help.push(data.pop());
        int temp = data.pop();
        help.push(temp);
        swap();
        return temp;
    }
    void push(int x)
    {
        data.push(x);
    }
    int pop()
    {
        if(data.empty())
        {
            cout<<"the stack is empty"<<endl;
            return NULL;
        }
        while(data.size()!=1)
            help.push(data.pop());
        int temp = data.pop();
        swap();
        return temp;
    }
    void swap()
    {
        queue<int>temp = data;
        data = help;
        help = data;
    }
};

26、猫狗队列

有三个类,宠物类、猫类、狗类。

实现一种猫狗队列结构,要求:

1、用户可以调用add方法将cat类或dog类的实例放入队列中。
2、pollall函数:按照先进先出原则,依次弹出所有实例。
3、polldag函数:先进先出弹出dog实例。
4、pollcat函数:先进先出弹出cat实例。
5、isempty函数:检查队列中是否还有实例。
6、isdogempty函数:检查队列中是否还有dog结构。
7、iscatempty函数:检查队列中是否还有cat结构。

方法:维护一个index,用来记录每个类是第几个进入队列的,在poll哪个类的index小,就先poll谁。

四、第四次课(2017.11.19)

27、哈希表和哈希函数

哈希函数:

  • 输入域是无穷大的,输出域是有限的。
  • 当输入参数一旦确定,参数固定,则返回值是相同的,不同的输入可能对应同一个输出。
  • 所有的输入值均匀地对应到输出上。

一致性哈希、缓存结构、负责均衡。

已有3台机器,但现在要添加1台,进行数据迁移,则取模的信息变了,如何解决?

引入一致性哈希,既能负载均衡,又能自由删减机器。

这个后面再去看,暂且放着

另:

1、如何构造散列函数:

  • 直接定址法:h(key)=a*key+b,取线性函数值作为散列地址。
  • 除留余数法:散列表表长为m,取不大于m但最接近m的质数p,h(key)=key%p。
  • 数字分析法:设关键字是r进制数,选取数码均匀的若干位作为散列地址。
  • 平方取中法:取关键字平方值的中间几位作为散列地址。
  • 折叠法:将关键字分割成位数相同的几部分,取这几部分的重叠和作为散列地址。

2、处理冲突的问题

  • 开放定址法:线性探测法、平方探测法、再散列法、伪随机序列法
  • 拉链法:将所有同义词存储在一个线性链表中,由散列地址唯一标识。
  • 散列查找法:取决于散列函数,处理冲突的方法和装填因子。

28、设计RandomPool结构

设计一种结构,在该结构中有如下三种功能:

  • insert(key):将某个key加入到该结构中,做到不重复加入。
  • delete(key):将原本在结构中的某个key移除。
  • getRandom(key):等概率随机返回结构中的任何一个key。

准备两张hash表。用size记录是第几个进来的。进则size++,删则size–。

如果要删除某个记录,将最后一行记录放入改记录中,删除最后一行记录,size–。

在等概论返回时,即可只返回当前size大小内的随机值所对应的key。rand()(size)。

29、转圈打印矩阵、转90度打印矩阵

29.1 转圈打印矩阵

要求额外空间复杂度为O(1)。

从左上角开始,列++,加到边后,行++,到边后,列- -,到边后,行- -。

重复上面四个过程。注意边界条件。

void print_one(vector<vector<int>>a, int tr, int tc, int dr, int tc)
{
    if(tc==dc)
    {
        for(int i=tr;i<=dr;i++)
            cout<<a[i][tc]<<endl;
    }
    else if(tr==dr)
    {
        for(int i=tc;i<=dc;i++)
            cout<<a[tr][i]<<endl;
    }
    else
    {
        int curr=tr;
        int curc=tc;
        while(curc < dc)
            cout<<a[tr][curc++]<<" ";
        while(curr < dr)
            cout<<a[curr++][dc]<<" ";
        while(curc>tc)
            cout<<a[dr][curc--]<<" ";
        while(curr>tr)
            cout<<a[curr--][tc]<<" ";
    }
}
void printmatrix(vetcor<vetcor<int>>a)
{
    int tr = 0;
    int tc = 0;
    int dr = a.size() - 1;
    int dc = a[0].size() - 1;
    while(tr<=dr && tc<=dc)
        print_one(a, tr++, tc++, dr--, dc--);
}

29.2 转90度打印矩阵

类似于转圈打印,原地调整。

主要内容的伪代码为:

int temp = a[tr][tc+i]  //temp = 列+i
a[tr][tc+i] = a[tr+i][dc]  //列+i=行+i
a[tr+i][dc] = a[dr][dc-i]  //行+i = 列-i
a[dr][dc-i] = a[dr-i][tc]   //列-i = 行 - i
a[dr-i][tc] = temp  //行-i = temp

30、之字型打印矩阵

一直找对角线,从左上角开始。

准备两个点a,b,作为记录对角线的两个端点位置。

a点从左上角开始,向右走,走到矩阵边界后就向下走。

b点从左上角开始,向下走,走到矩阵边界后就向右走。

每次a,b都各走一步,然后打印以a,b为端点的对角线上的元素。每次用一个bool值记录打印的方向,用来标记方向交替。

31、在行列有序的矩阵中找数

时间复杂度为O(M+N)。

从右上角开始,如果要查找得数x比右上角这个数大,则继续向下找,否则就向左找。

直到走到边界,可判断这个数是否在矩阵中。

bool if_find(vecotr<vector<int>>a, int x)
{
    int i=0;
    int j=a[0].size()-1;
    while(i<a.size() && j>=0)
    {
        if(a[i][j] == x)
            return true;
        else if(a[i][j] > x)
            j--;
        else
            i++;
    }
    return false;
}

32、打印两个有序链表的公共部分

33、判断一个链表是否是回文结构

34、将单向链表按照某值划分为左边小、中间相等、右边大结构,且内部保持原有顺序

35、复制含有随机指针节点的链表

36、两个单链表相交的一系列问题

37、反转单向、双向链表

五、第五次课(2017.11.25)

38、随时找到数据流的中位数

39、金条切块

40、求收益问题

41、折纸问题

42、二叉树的前序、中序、后序遍历的递归与非递归实现

42.1 前序

递归写法:

void preOrder(TreeNode *root)
{
    if(root==NULL)
        return ;
    cout<<root->val<<" ";
    preOrder(root->left);
    preOrder(root->right);
}

非递归:

利用栈,将头节点放入栈中,弹出并打印。然后将右节点和左节点依次入栈。重复弹出栈顶并打印,右、左依次入栈的过程。

void oreOrder(TreeNode *root)
{
    if(root==NULL)
        return ;
    stack<TreeNode*>s;
    s.push(root);
    TreeNode *p = root;
    while(!s.empty())
    {
        p = s.pop();
        cout<<p->val<<" ";
        if(p->right!=NULL)
            s.push(p->right);
        if(p->left!=NULL)
        s.push(p->left);
    }
}

42.2 中序

递归:

void inOrder(TreeNode *root)
{
    if(root==NULL)
        return ;
    inOrder(root->left);
    cout<<root->val<<" ";
    inOrder(root->right);
}

非递归:

利用栈,如果头节点不为空,则入栈,头节点等于其左孩子。一直到头节点为空,则弹出栈顶,并且将栈顶右孩子入栈。

void inOrder(TreeNode *root)
{
    if(root==NULL)
        return ;
    stack<TreeNode *>s;
    s.push(root);
    TreeNode *p=root;
    while(!s.empty() || p!=NULL)
    {
        if(p!=NULL)
        {
            p = p->left;
            s.push(p);
        }
        else
        {
            p = s.pop();
            cout<<p->val<<" ";
            p = p->right; 
        }
    }
}

42.3 后序

递归:

void postOrder(TreeNode *root)
{
    if(root==NULL)
        return ;
    postOrder(root->left);
    postOrder(root->right);
    cout<<root->val<<" ";
}

非递归:

利用栈和前序的非递归遍历,前序是头节点入栈,弹出并打印,右左节点依次入栈,重复弹出栈顶打印。

这样得到的是中左右,我们可以得到中右左,就是按照前序非递归方法,头节点入栈,弹出并打印,左右节点依次入栈,重复弹出栈顶打印。

这样得到中右左后,逆序后就是左右中,即后序遍历。

需要准备两个栈。

void postOrder(TreeNode *root)
{
    if(root==NULL)
        return ;
    stcak<TreeNode *>help;
    stcak<TreeNode *>s;
    help.push(root);
    TreeNode *p=root;
    while(!help.empty())
    {
        p = help.pop();
        s.push(p);
        if(p->left)
            help.push(p->left);
        if(p->right)
            help.push(p->right);
    }
    while(!s.empty())
    {
        p = s.pop()
        cout<<p->val<<" ";
    }
}

43、较为直观的打印二叉树

先从最右边开始打印。

44、在二叉树中找到一个节点的后继节点

后继节点:对二叉树进行中序遍历后,一个节点的后一个节点就是后继节点。即树的最右边的那个节点(中序遍历最后一个节点)是没有后继节点的。

建立一个新型二叉树,有val、左节点、右节点、父节点。

对于节点s,

如果有右子树,则后继节点为右子树上最左的节点。

如果没有右子树,但是是父亲的左孩子,则父亲节点就是后继节点;如果是父亲的右孩子,则继续向上找。

TreeNode *getleftnode(NodeTree *root)
{
    if (root==NULL)
        return NULL;
    while(root->left!=NULL)
        root = root->left;
    return root;
}
TreeNode *getnode(NodeTree *root)
{
    if(root==NULL)
        return NULL;
    if(root->right!=NULL)
        return getleft(root->right;)
    else
    {
        TreeNode *parent = root.parent;
        while(parent != NULL && parent->left != NULL)
            parent = parent->parent;
        return parent;
    }
}

45、并查集

46、布隆过滤器

六、第六次课(2017.11.26)

47、二分搜索

48、前缀树及其六个应用

前缀树是字典树,没有左右孩子之分,是多叉树。

例如,将abc、bcd、abd、bck生成共同前缀树,则为:

      o
   a/  \b
    o   o
  b/      \c
  o        o
c/ \d    d/  \k
o   o     o  o

从空节点开始,用边来表示字符,有这个边的话,就共用,没有就加边。

48.1 找B数组中哪些串是A数组中串的前缀

48.2 查找某个字符串是否在数组中

48.3 查找某个字符串出现否,且出现几次

48.4 二叉树曾经有多少个路径经过当前节点

48.5 删除字符串

48.6 整型数组中都是3位数,是否能得到最趋近999的和

49、图、无向图、有向图、邻接表、邻接矩阵

50、图的宽度优先遍历

51、图的深度优先遍历

52、拓扑排序

53、最小生成树:Kruskal算法、Prim算法

54、Dijkstra算法

七、第七次课(2017.12.02)

55、递归概念

56、汉诺塔问题

57、打印一个字符串的全部子序列

58、打印一个字符串的全部序列

59、母牛生小牛问题、人走台阶问题

60、给定数字字符串,转换成字母字符串

61、逆序栈,不用额外空间,递归

62、动态规划概念

63、数组从左上走到右下的最小路径和

64、数组中的数是否能累加到aim

65、01背包问题

八、第八次课(2017.12.03)

66、堆的应用、攻克城市

67、数组代表容器,求max装水量

68、最大的leftmax与rightmax之差的绝对值

69、求最大子数组和

70、字符串是否可循环右移得到某字符串

71、字符串右移K位后的字符串,原地调整

72、生成窗口最大值数组

73、拼接字符串,使其字典顺序最小

74、占用会议室问题

九、第九次课(2017.12.06)

75、Morris遍历

76、搜索二叉树

77、平衡树AVL

78、SB树、红黑树

79、跳表

猜你喜欢

转载自blog.csdn.net/liuxiao214/article/details/79593334