【基础复习】九:数据结构基础


重要的复习:算法与数据结构的系列博客

单链表

单链表实现增、删、查(不知道链表大小的时候返回中间位置、打印、返回大小)、改(逆置)。
自己随便写一个实现一下:

#include<iostream>
#include<string>
#include<sstream>
using namespace std;

template<typename T> 
struct Single_Node {
    T val;
    Single_Node* next;
    Single_Node(){}
    Single_Node(T t) :
     val(t) {next = NULL;}
};

template<typename T>
class Single_list;

template<typename T>
ostream& operator<<(ostream& out, const Single_list<T>& list);

template<typename T>
class Single_list {
  public:
    //单向链表
    Single_list() {
        head = NULL;
    }

    //顺序插入
    Single_Node<T>* insert(T t) {
        Single_Node<T>* com = new Single_Node<T>(t);
        if (head==NULL) {
            head = com;
        } else {
            Single_Node<T> *p1, *p2;
            //p2-->p1
            p1 = head;
            while (p1->next!=NULL && p1->val <= t) {
                p2 = p1;
                p1 = p1->next;
            }

            if (p1->val > t) {
                if (p1==head) {
                    com->next = head; 
                    head = com;
                } else {
                    p2->next = com;
                    com->next = p1;
                }
            } else {
                p1->next = com;
            }
        }
    }

    //删除第一个等值的
    void del(T t) {
        if (head==NULL) {
            cout << "no " << t << " node" << endl;
        } else {
            Single_Node<T>* p1, *p2;
            p1 = head;
            while (p1->val!=t && p1->next!=NULL) {
                p2 = p1;
                p1 = p1->next;
            }

            if (p1->val == t) {
                if (head == p1) {
                    head = head->next;
                    delete p1;
                } else {
                    p2->next = p1->next;
                    delete p1;
                }
                cout << "delete " << t << " node successfully!" << endl;
            } else {
                cout << "no " << t << " node" << endl;
            }
        }
    }

    //链表逆置
    void reverse() {
        if (head==NULL || head->next==NULL) {
            return;
        } else {
            Single_Node<T> *p1, *p2, *p3;
            p1=head;
            p2=p1->next;
            while(p2) {
                p3 = p2->next;
                p2->next = p1;
                p1 = p2;
                p2 = p3;
            }
            head->next = NULL;
            head = p1;
        }
    }

    //不知道链表大小的情况下,返回链表中间节点
    Single_Node<T>* mid() {
        Single_Node<T> *p1, *p2;
        p1 = p2 = head;
        if (p1==NULL || p1->next ==NULL) {
            return p1;
        } else {
            while(p1->next!=NULL && p1->next->next!=NULL) {
                p2 = p2->next;
                p1 = p1->next->next;
            }
            return p2;
        }
    }

    ~Single_list() {
        Single_Node<T> *p1, *p2;
        p1 = head;
        while (p1!=NULL) {
            p2 = p1;
            p1 = p1->next;
            delete p2;
        }
        head = NULL;
    }

    //返回大小
    int size() {
        int i;
        Single_Node<T>* p = head;
        for (i = 0; p != NULL; i++) {
            p = p->next;
        }
        return i;
    }

    friend ostream& operator<< <T>(ostream& out,  const Single_list<T>& list);
  private:
    Single_Node<T>* head;
};


template<typename T>
ostream& operator<<(ostream& out,  const Single_list<T>& mylist) {
    Single_Node<T> *p = mylist.head;
    while (p!=NULL) {
        out << p->val << endl;
        p = p->next;
    }
    return out;
}

int main() {
    Single_list<int> mylist;
    for (int i = 0; i < 5; i++) {
        mylist.insert(i);
    }

    cout << mylist.mid()->val << endl
         << "-------------------" << endl
         << mylist
         << "-------------------" << endl;

    for (int i = 10; i > 5; i--) {
        mylist.insert(i);
    } 

    cout << mylist.mid()->val << endl
         << "-------------------" << endl
         << mylist 
         << "-------------------" << endl;

    mylist.del(5);
    cout << mylist
         << "-------------------" << endl;
    mylist.del(6);
    cout << mylist
         << "-------------------" << endl;

    mylist.reverse();
    cout << mylist
         << "-------------------" << endl;

    cout << mylist.size() << endl;
    cout << mylist.mid()->val << endl;

}

双链表

就比单链表多了一个向前的指针QAQ我应该会吧

循环链表

就是最后一个元素指回第一个元素QAQ我应该会吧

队列

先进先出QAQ我应该会吧

先进后出QAQ我应该会吧

1.【内存意义上的堆】
堆和栈的区别
2.【数据结构意义上的堆】

树、图、哈希表

  • 高度为k的树最多有2^k-1个点

  • 只知道先序和后序无法确定一棵树

  • 已知先序/后序和中序,可以确定一棵树

  • 二叉搜索树的左子树小于根,右子树大于根

  • 平衡二叉树:要么是一棵空树,或者它的左右两个子树的高度茬绝对值不超过1,,且左右子树也是平衡树

  • n个点的无向图最有多n(n-1)/2条边

  • 有向图拓扑排序算法基本步骤如下:

    • 从图中选择一个入度为0的顶点,输出该点
    • 从图中删除该定点及其相关的弧,调整相关顶点的入度
    • 重复前面两个步骤,直到所有点都输出

面试题1:在百度或者淘宝搜索时,每输入字符都会出现搜索建议,比如输入“北京”,搜索框下面会以北京为前缀,展示“北京爱情故事”、“北京公交”、“北京医院”等等搜索词。实现这类技术后台所采用的数据结构是什么?
答案:
Trie树,又称单词查找树、字典树,是一种树形结构,是一种哈希树的变种,是一种用于快速检索的多叉树结构。典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。
它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高。Trie树的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。
对于搜索引擎,一般会保留一个单词查找树的前N个字(全球或最近热门使用的);对于每个用户,保持Trie树最近前N个字为该用户使用的结果。
如果用户点击任何搜索结果,Trie树可以非常迅速并异步获取完整的部分/模糊查找,然后预取数据,再用一个web应用程序可以发送一个较小的一组结果的浏览器。


**面试题2:**一个包含n个节点的四叉树,每一个节点都有4个指向孩子节点的指针,这个四叉树有几个空指针? **答案:** 一共n个节点,每个节点4个指针,一共4n个指针。有n个节点,其中除了根节点,有n-1个节点被指针指着,也就是n-1个非空指针。所以空指针=4n-(n-1)=3n+1
**面试题3:**有1千万条短信,有重复,以文本文件的形式保存,一行一条,有重复。请用5分钟时间,找出重复出现最多的前10条。 **答案:** 类似的题目是如何根据关键词搜索访问最多的前10个网站。 **方法一:** 可以用哈希表的方法对1千万条分成若干组进行边扫描边建散列表。第一次扫描,取首字节、尾字节、中间随便两字节作为Hash Code,插入到hash table中。并记录其地址和信息长度和重复次数,1千万条信息,记录这几个信息还放的下。同hash co等长就是疑似相同,比较一下。相同记录只加1次进hash table,但将重复次数加1.一次扫描以后,已经记录各自的重复次数,进行第二次hash table的处理。用线性时间选择可在O(n)的级别上完成前10条的寻找。分组后每份中的top10必须保证各不相同,可hash来保证。

方法二:
可以采用从小到大的排序的办法,根据经验,除非是群发的过节短信,否则字数越少的短信出现重复的几率越高。建议从字数少的短信开始找起,比如一开始搜一个字的短信,找出重复出现的top10并分别记录出现次数,然后搜两个字的,依次类推。对于对相同字数的比较长的短信的搜索,除了hash之类的算法外,可以选择只抽取头、中和尾等几个位置的字符进行粗判,因为此种判断方式是为了加快查找速度但未必能得到真正期望的top10,因此需要做标记;如此搜索一遍后,可以从各次top10结果中找到备选的top10,如果这top10中有刚才做过标记的,则对其对应字数的所有短信进行精确搜索以找到真正的top10并再次比较。

方法三:
可以采用内存映射的办法,首先,1千万条短信按现在的短信长度将不会超过1G空间,使用内存映射文件比较合适。可以一次映射(当然如果更大的数据量的话,可以采用分段映射),由于不需要频繁使用文件I/O和频繁分配小内存,这将大大提高数据的加载速度。其次,对每条短信的第i(i从0到70)个字母按ASCII码进行分组,其实也就是创建树。i是树的深度,也是短信第i个字母。
该问题主要是解决两方面的内容,一是内容的加载,二是短信内容比较。采用文件内存映射技术可以解决内容加载的性能问题(不仅仅不需要调用文件I/O函数,而且也不需要每读出一条短信都分配一小块内存),而使用树技术可以有效减少比较的次数。
代码如下

struct TNode
{
    BYTE* pText;
    //直接指向文件映射的内存地址
    DWORD dwCount;
    //计算器,记录此节点的相同短信数
    TNode* ChildNodes[256];
    //子节点数据,由一个字母的ASCII值不超过256,所以子节点也不可能超过256
    TNode() {
        //初始化成员
    }
    ~TNode() {
        //释放资源
    }
};

//int nIndex是字母下标
void CreateChildNode(TNode* pNode, const BYTE* pText, int nIndex) {
    if (pNode->ChildNodes[pText[nIndex]] == NULL) {
        //如果不存在此子节点,就创建,TNode构造函数
        //为了处理方便,这里也可以在创建的同时把此节点加到一个数组中
        pNode->ChildNodes[pText[nIndex]] = new TNode;
    }

    if (pText[nIndex+1] == '') {
        //此短信已完成,计数器加1,并保存此短信内容
        pNode->ChildNodes[pText[nIndex]]->dwCount++;
        pNode->ChildNodes[pText[nIndex]]->pText = pText;
    } else {
        //如果还没结束,就创建下一级节点
        CreateNode(pNode->ChildNodes[pText[nIndex]], pText, nIndex+1);
    }
}

//创建根节点,pTexts是短信数组,dwCount是短信数量(这里是1千万)
void CreateRootNode(const BYTE* pTexts, DWORD dwCount) {
    TNode RootNode;
    for (DWORD dwIndex = 0; dwIndex<dwCount; dwIndex++) {
        CreateNode(&RootN, pTexts[dwIndex], 0);
    }

    //所有节点按dwcount的值进行排序
    //取前10个节点,显示结果
}

**扩展知识:** 有1亿个浮点数,请找出其中最大的10000个。 **解析:** 假设每个浮点数占4个字节,1亿个浮点数就要占到相当大的空间,因此不能一次将全部读入内存进行排序。

方法1:
读出100万个数据,找出最大的1万个,如果这100万数据选择够理想,那么最小的这1万个数据里面最小的为基准,可以过滤掉1亿数据里面99%的数据,最后就再一次在剩下的100万(1%)里面找出最大的1万个。

方法2:(我好像没看懂)
分块查找,比如100万一个块,找出最大1万个,一次下来就是横下100万数据需要找出1万个。
找出100万个数据里面最大的1万个,可以采用快速排序的方法,分2堆,如果大堆的那堆个数N大于1万个,继续对大堆快速排序一次分成2堆,如果大堆个数N小于1万,就在小的那堆里面快速排序一次,找第10000-N大的数字。递归以上过程,就可以找到相关结果。

排序

常用排序算法总结(性能+代码)
会实现主要的几种排序算法,以及了解下面的两张表格,应该就没有问题了吧QAQ

稳定的排序:

排序方法 时间复杂度 空间复杂度
冒泡排序 最差、平均都是O(n^2), 最好是O(n) 1
插入排序 最差、平均都是O(n^2), 最好是O(n) 1
归并排序 最差、平均、最好都是O(nlogn) O(n)
桶排序 O(n) O(k)
基数排序 O(dn)(d是常数) O(n)

不稳定的排序:

排序方法 时间复杂度 空间复杂度
选择排序 最差、平均都是O(n^2) 1
希尔排序 O(nlogn) 1
堆排序 最差、平均、最好都是O(nlogn) 1
快速排序 平均是O(nlogn),最坏情况O(n^2) O(logn)

时间复杂度

维基百科:时间复杂度
算法的时间复杂度和空间复杂度-总结
分析时间复杂度的一些练习

  1. 下面哪个选项正确描述了代码运行的调度次数?
n = 10;
for (int i = 1; i < n; i++) {
    for (int j = 1; j < n; j+=n/2) {
        for (int k = 1; k < n; k *= 2) {
            x++;
        }
    }
}

解析:
O(nlogn)

2.有序队列数据相对于无序队列数据,下列哪种操作并不快?
A. 找出最小值 B.估算平均值 c.找出中间值 D.找出最大出现可能性
解析:

  • 对于寻找最小值:有序是O(1), 无序是O(n)
  • 对于估算平均值:有序是O(n), 无序是O(n)
  • 对于找出中间值:有序是O(1), 无序是O(n)(类似于快排的算法)
  • 对于找出最大可能性:有序是O(n), 无序是O(logn)(使用平衡查找结构而不是哈希表) (这个没有很懂,无序直接扫描一遍不是也能确定出现最多的吗?)

答案:B

Top-K问题与多路归并排序

3.谷歌面试题:辗转相除法的时间复杂度?
答:欧几里得算法,又称辗转相除法,用于求两个自然数的最大公约数。算法的思想:gcd(a, b)=gcd(b, a mod b), 其时间复杂度为O(logn)

最后是我不知道为什么会放在这一章的云计算面试题集

字符串

字符串的经典问题和算法
C++面试题之字符串

补充:

1.求一个字符串中连续出现次数最多的子串,请给出分析和代码。
解析:
这个题目可以首先逐个子串扫描来记录每个子串出现的次数。比如:abc这个字串,对应子串为a/b/c/ab/bc/abc,各出现一次,然后再逐渐缩小字符子串得出正确的结果。
代码如下:

#include<iostream>
#include<vector>
#include<cstring>
#include<string>
using namespace std;

pair<int, string> fun(const string &str) {
    vector<string> subs;
    int maxcount = 1, count = 1;
    string substr;
    int i, len = str.length();

    //将部分子串入栈
    for (i = 0; i < len; i++)
        subs.push_back(str.substr(i, len-i));

    for (i = 0; i < len; i++) {
        for (int j = i+1; j <= (len+i)/2; j++) {
            count = 1;
            //对栈中的这些子串的子串再进行比较
            if (subs[i].substr(0, j-i) == subs[j].substr(0, j-i)) {
                count++;
                for (int k = j+(j-i); k < len; k += j-i) {
                    if (subs[i].substr(0, j-i) == subs[k].substr(0, j-i))
                        count++;
                    else
                        break;
                }

                //更新最大的记录
                if (count > maxcount) {
                    maxcount = count;
                    substr = subs[i].substr(0, j-i);
                }
            }
        }
    }

    return make_pair(maxcount, substr);
}

pair<int, string> fun1(const string& str) {
    int maxcount = 1, count = 1;
    string substr;
    int i = 0, j = 0;
    int len = str.length();

    int k = i+1;
    while (i < len) {
        //从k~len-1范围内寻找str[i]
        j = str.find(str[i], k);

        //若找不到说明k~len-1范围内没有str[i]
        if (j == string::npos || j >(len+i)/2 ) {
            i++;
            k = i+1;
        } else {
            int s = i;    //若找到必有j>=i+1
            int s1 = j-i; //连续字串的步长

            count = 1;
            while (str.substr(s, s1) == str.substr(j, s1)) {
                //检测连续字串是否相等
                count++;
                s = j;
                j = j+s1;
            }

            //更新最大的记录
            if (count > maxcount) {
                maxcount = count;
                substr = str.substr(i, s1);
            }

            k = j+1;
        }
    }

    return make_pair(maxcount, substr);
}


int main() {
    string str;
    pair<int ,string> rs;
    while(cin >> str) {
        rs = fun(str);
        cout << rs.second << ":" << rs.first << endl;
        rs = fun1(str);
        cout << rs.second << ":" << rs.first << endl;
    }

    return 0;
}

2.写一个函数来模拟c++中的strstr()函数:该函数的返回值是主串中字符子串的位置以后的所有字符。请不要使用任何c程序已有的函数来完成。

#include<iostream>
using namespace std;

//基本就是暴力搜
const char* strstr1(const char* string, const char* strCharSet) {
    for (int i = 0; string[i] != ''; i++) {
        int j = 0;
        int temp = i;
        if (string[i] == strCharSet[j]) {
            while(string[i++] == strCharSet[j++]) {
                if ((strCharSet[j] == ''))
                    return &string[i-j];
            }
            i = temp;
        }
    }
    return NULL;
}

int main() {
    char* string = "12345554555123";
    cout << string << endl;
    char strCharSet[10] = {};
    cin >> strCharSet;
    cout << strstr1(string, strCharSet) << endl;

    return 0;
}

3.转换字符串格式为原来字符串里的字符+该字符连续出现的个数,例如字符串1233422222,转化成1121324125.(1出现1次,2出现1次,3出现2次......)
解析:
可以通过sprintf来实现算法。

  • 打印字符串。在大多数场合sprintf可以替代itoa,把整数打印到字符串中。
  • 连接字符串。在大多数场合可以替代strcat,能一次连接多个字串并同时在之间插入一些别的东西。
#include<iostream>
#include<string>
#include<cstring>
#include<cstdio>
using namespace std;

int main() {
    cout << "Enter the numbers " << endl;
    string str;
    char reschar[50];
    reschar[0] = '';
    getline(cin , str);
    int len = str.length();
    int count = 1;
    int k;

    //基本就是简单模拟做下来
    for (k = 0; k <= len-1; k++) {
        if (str[k+1] == str[k]) {
            count++;
        } else {
            sprintf(reschar+strlen(reschar), "%c%d", str[k], count);
            count = 1;
        }
    }

    if (str[k] == str[k-1])
        count++;
    else
        count = 1;
        
    sprintf(reschar+strlen(reschar), "%c%d", str[k], count);
    cout << reschar << endl;        
}

4.实现字符串循环右移n位,和上面的链接里面的一道一样。但是这次实现不利用库函数。
解析:
利用最后存放''的那一位,来模拟右移一步的过程,一共m次。

#include<iostream>
#include<cstring>
using namespace std;

void fun(char* w, int m) {
    int i = 0;
    int len = strlen(w);
    if(m>len)
        m = len;

    while(m--) {
        for (int i = len-1; i >= 0; i--)
            w[i+1] = w[i];
        w[0] = w[len];
    }
    w[len] = '';
}

int main() {
    char s[] = "ABCDEFGHI";
    fun(s, 3);
    cout << s << endl;
}



from《程序员面试宝典》



原文地址:https://www.cnblogs.com/zengyh-1900/p/5256390.html