这是一道关于数据结构的查找的习题,查找的对象是字符串,可能和课本上以整型数据为对象的例题不太相同,所以我自己想了一种做法,得益于C++强大的string类,这道题用C++解决起来比较方便。
题目大体是这样的,程序会接受N个用户名,名字长度不超过100,没有按照规定的规则排序,要求我们按照输入时的顺序,检查每一个特定的名字前面是否有和它相同的名字,当每一个名字都检查完毕后,再按照顺序输出Yes或者No,yes代表该位置的名字前面有和它相同的名字,no代表该位置的名字前面没有和它相同的名字,检查对于大小写是不敏感的,也就是说如果输入的第一个名字是A,输入的第三个名字是a,那么在输出的第三行就是Yes,第一行就是No,因为A没有前面的名字可比,它是第一个;程序输入的第一行是一个数字,代表需要处理的名字的个数,然后是按行输入名字;输出则是给出每个名字按照检索规则对应的结果,行数是和输入中该名字的行数相对应的。题目给出了一个输入输出的例子
样例输入:
5//即将输入程序的名字的个数 AbC jisuanke abc JiSuanKe Hello
样例输出:
No No Yes Yes No
我的思考过程是这样的,这个题用我们平时对整型对象的处理方式非常不方便,因为字符串的区别在于内容,是没有办法单一地比较的,实数就不一样,实数的一个性质就是可以比较大小,所以我们在查找整数或者小数时有许多处理方法,顺序查找、折半查找、分块查找……大体的思路都是利用它们的可比性先对它们进行简单的处理,然后再设计规则查找数据,但是我们这里的对象是字符串,而且要求对每一个字符串进行相应的查找,所以需我们设计合适的方法。由于需要对每一个名字进行处理,所以我刚开始设计了一种非常笨重的办法,就是每输入一个数据就进行一次循环,循环的内容是检查这个名字之前的所有名字是否有和它相同的,如果有就把这个名字的标志记为“Yes",没有则记为"No"。然后测试的时候时间就超限了,我们也可以看出来这种方法程序的开销太大了,假如需要输入n个名字,那么需要的比较的次数就是1!+2!+3!+……n!;然后就开始改进方法,这时我注意到题目的一些暗示,在叙述判断名字相同的规则时,题目这样说:“不会对大小写敏感,对于名字长度一样且对应位置字符相同的名字,就认为它们是一样的”,所以我们可以从名字的长度入手,先对所有名字进行处理,然后对于名字长度相同的名字进行比较,缩小每个名字的比较范围这样就可以大大减少没用的比较,所谓没用的比较,就是明明知道比较的结构,但是由于程序的按部就班,只能去执行没有收益的操作,这是由于算法在设计上的不合理造成的,就像我们在第一种方法里面的情况,比如第一个名字输入15a(长度为3),第二个名字输入i(长度为1),两者完全没有比较的必要,但是由于算法在设计上要求对某个具体名字之前的所有名字进行检查,所有就多了许多无用功。改进后可以很好避免上述情况。
这种思想有点类似分块查找的方式,在学习数据结构过程中,我对查找算法的组织其实是这样的,查找算法分为三大类,顺序查找(就是每个都遍历一遍),索引查找(比如哈希表),组织树来辅助查找(比如AVL树);而顺序查找的改进方式其实就是通过缩小查找范围,去除无用的遍历,比如折半查找(感觉非常像二分法求某个数的根,高中数学,数学巅峰),分块查找,他们的基本思路就是遍历,只不过是在遍历前对范围进行划分或者对遍历的目的按照某种方式约束,以此来降低遍历开销。
详细代码如下:
1 #include<iostream> 2 #include<string> 3 #include<algorithm> 4 using namespace std; 5 struct node 6 { 7 8 string name;//名字内容 9 int wik;//所处位置的记录量 10 //一个结构,内含名字和该名字在输入的名字中所处的位置 11 node* next; 12 };//用来存放长度相同的名字的链表 13 int main() 14 { 15 int n; 16 node* head[101];//假定最长的名字为100; 17 for (int i = 1; i <= 100; i++) 18 head[i] = NULL;//初始化头节点为NULL; 19 cin >> n; 20 string* s = new string[n]; 21 string* k = new string[n]; 22 for (int i = 0; i < n; i++) 23 { 24 cin >> s[i]; 25 transform(s[i].begin(), s[i].end(), s[i].begin(), ::tolower);//把大写的字符串都转换为小写的字符串 26 if (head[s[i].size()] ==NULL) 27 { 28 node* p= new node; 29 p->name = s[i]; 30 p->wik = i; 31 head[s[i].size()] = p; 32 p->next = NULL; 33 } 34 else if(head[s[i].size()]!=NULL) 35 { 36 node* p = new node; 37 p->name = s[i]; 38 p->wik = i; 39 p->next = head[s[i].size()]; 40 head[s[i].size()] = p;//把新加入该长度序列的u插入到该链表头; 41 } 42 } 43 //node* temp; 44 /*for (int i = 1; i <= 100; i++) 45 { 46 temp = head[i]; 47 cout << i << ":"; 48 while (temp) 49 { 50 cout << temp->name << " "; 51 temp = temp->next; 52 } 53 } 54 *///注释掉的内容是显示各种长度的链表内名字的收纳情况,可以使之运行出来方便理解。 55 for (int i = 1; i <= 100; i++)//100种可能长度都要找 56 { 57 node* temp; 58 temp = head[i]; 59 string st; 60 while (temp) 61 { 62 st = temp->name; 63 node* heap = temp->next;//找每一个链表节点,查看其后续节点是否与其相同。 64 while (heap) 65 { 66 if (st == heap->name) 67 { 68 k[temp->wik] = "Yes"; 69 break; 70 } 71 heap = heap->next; 72 } 73 if(k[temp->wik]!="Yes") 74 k[temp->wik] = "No"; 75 temp = temp->next; 76 } 77 } 78 for (int i = 0; i < n; i++) 79 cout << k[i] << endl; 80 return 0; 81 }
从上可以看出,我定义了一个结构体node,head是一个node指针数组,用来保存各种长度的名字构成的链表(头指针),而且数组的下标为该链表内保存的名字对应的长度,就相当于把我们需要处理的名字按照长度分了个类,而且在排列上是先输入的名字在链表中靠后链,比如我们输入长度都是4的名字,按照先后,一个jack,一个mary,一个chen,一个Jack,那么head[4]指向这样一个链表,头指针指向Jack,即:Jack—chen—mary—jack,这样处理每一个名字时只需要从前往后依次处理就行,比如对于Jack,我们从Jack起依次比较后面的元素,有相同就把Jack的标志改为"Yes“,没有就标志为”No“,然后处理mary,……,如果不是这样生成链表的,那么在对每个名字处理起来可能会非常不方便,比如head[4]是这样的jack—mary—chen—Jack,那么我们需要倒着检查每个名字,如果非要正着,从jack入手,那可能需要一个双向链表了。
然后题目要求的输出方式的解决方法,我是设置了一个k字符串数组,用来保存每个名字对应的判断结果,我们保存名字用s,比如s[12] 是“an”,前面没有和它同名的,那么k[12]就是”No"。k和链表以及s的协同就是靠node结构体中的一个元素wik,如果看程序费劲,我们模拟一个名字的处理过程就知道了:比如第15个名字输入“Tom”,那么Tom会被纳入head[3]( 因为它的长度就是3,这在输入时就顺带处理完了),程序接受到Tom,就进入head[3]中,申请一个node空间,录入name为Tom,wik为15,然后把它插入head[3]所指向的链表的表头,然后去录入第16个名字;当检查各个名字时,程序会处理到head[3],假如找到了一个“tom”在11个(在head[3]中它应该是链在Tom后面的),11比15靠前,那么k[15]就是“Yes”;在输出过程中,当输出到第15个名字的判断结果时,就读取k[15],然后我们就看到了“Yes”,与输入次序相应的Tom的输出。
这里string的函数size,以及重载的“==”帮了非常大的忙,转换函数transform也非常好用,(因为没有后续操作了,所以我们直接把原数据处理了,方便我们做判断,这在其他情况下不一定可取)。
程序运行实例如下: