程序员编程艺术学习笔记(四)现场编写类似strstr/strcpy/strpbrk的函数

程序员编程艺术原文出自大神:v_JULY_v  地址:http://blog.csdn.net/v_july_v/article/category/784066

c++实现.敬请膜拜。

字符串问题虽基础,变化却是令人发指=—=,随便改改就能让人纠结半天。。继续砸瓷实基础,基础,基础。

原文C++实现,现将自己实现的java版本记录如下,不定期更新,自我监督,自我监督。


跳过了第三章的排序内容,因为需要去了解一些细节

继续提升编程能力和习惯,先把之后的实现。


第一节、字符串查找
1.1题目描述:
给定一个字符串A,要求在A中查找一个子串B。
如A="ABCDF",要你在A中查找子串B=“CD”。

常用的两种方式,暴力破解法和KMP算法。

KMP算法要解决的问题就是在字符串(也叫主串)中的模式(pattern)定位问题。说简单点就是我们平时常说的关键字搜索。模式串就是关键字(接下来称它为P),如果它在一个主串(接下来称为T)中出现,就返回它的具体位置,否则返回-1(常用手段)。

 

首先,对于这个问题有一个很单纯的想法:从左到右一个个匹配,如果这个过程中有某个字符不匹配,就跳回去,将模式串向右移动一位。这有什么难的?

我们可以这样初始化:

 

之后我们只需要比较i指针指向的字符和j指针指向的字符是否一致。如果一致就都向后移动,如果不一致,如下图:

 

 

A和E不相等,那就把i指针移回第1位(假设下标从0开始),j移动到模式串的第0位,然后又重新开始这个步骤:

 


暴力破解法,时间复杂度O(n*m)【我们知道实际应用中这种方式消耗就很高了】

(注:在代码风格异常判断的时候,多看几种代码,养成采众家之长的习惯。干净漂亮规整的代码,对后期发展的好处不是一点噢)

主体代码如下:

public class StrStr {
	public static void main(String args[]){
		String str1="ABCDEFGHIJKLMN";
		String str2="GHI";
		String str3="WEREIJG";
		System.out.println(getInNum(str1,str2));
		
		
	}
	public static int getInNum(String str1,String str2){
		int num=0;
		
		if(str1==null||str2==null){
			return -1;
		}
		char[] a=str1.toCharArray();
		char[] b=str2.toCharArray();
		if(a.length<b.length){
			return -1;
		}
	
		int i=0;//主串位置
		int j=0;//子串位置
		
		while(i<a.length&&j<b.length)	{	
		if(a[i]==b[j]){// 当两个字符相同,就比较下一个
			i++;
			j++;
		}else{
			i=i-j+1;// 一旦不匹配,i后退
			j=0;// j归0
		}
		}
		//开始对于循环外判断有疑问,debug发现j已经不小于b.length了 循环被跳出
		if(j==b.length){
			return i-j;
		}
		else{
			return -1;
		}
	}
	
	
	
	//个人倾向第一种方式 这个是原作者方式,个人感觉逻辑不够清晰 返回的i+1也值得商榷
	public static int getInNum2(String str1,String str2){
		int num=0;
		
		if(str1==null||str2==null){
			return -1;
		}
		char[] a=str1.toCharArray();
		char[] b=str2.toCharArray();
		if(a.length<b.length){
			return -1;
		}
		int i,j;
		for(i=0;i<a.length;i++){
			for(j=0;j<b.length;j++){
				if(a[i+j]!=b[j]){
					break;
				}
				
			}
			if(j==b.length){
				return i+1;
			}
				
		}
		return -1;
		
	}
}


KMP方法

KMP算法看了很多,每一个都挺详细但是清晰的不多,下边列举几种说明,也许单独看某个说明不好理解,结合起来看,就可以明白位移量的关键原理了。


http://billhoo.blog.51cto.com/2337751/411486

【KMP算法思想详述与实现】
 
        前面提到,KMP算法通过一个“有用信息”可以知道目标串中下一个字符是否有必要被检测,这个“有用信息”就是用所谓的“前缀函数(一般数据结构书中的next函数)”来存储的。
        这个函数能够反映出现失配情况时,系统应该跳过多少无用字符(也即模式串应该向右滑动多长距离)而进行下一次检测,在上例中,这个距离为4.
        总的来讲,KMP算法有2个难点:
              一是这个前缀函数的求法。
              二是在得到前缀函数之后,怎么运用这个函数所反映的有效信息避免不必要的检测。
下面分为两个板块分别详述:
 
 
【前缀函数的引入及实现】
 
【前缀函数的引入】
        对于前缀函数,先要理解前缀是什么:
        简单地说,如字符串A = "abcde"        B = "ab"
        那么就称字符串B为A的前缀,记为B ⊏ A(注意那不是"包含于",Bill把它读作B前缀于A),说句题外话——"⊏"这个符号很形象嘛,封了口的这面相当于头,在头前面的就是前缀了。
        同理可知 C = "e","de" 等都是 A 的后缀,以为C ⊐ A(Bill把它读作C后缀于A)
      
理解了什么是前、后缀,就来看看什么是前缀函数:
        在这里不打算引用过多的理论来说明,直接引入实例会比较容易理解,看如下示例:
 
 
      (下述字符若带下标,则对应于图中画圈字符)
      这里模式串 P = “ababaca”,在匹配了 q=5 个字符后失配,因此,下一步就是要考虑将P向右移多少位进行新的一轮匹配检测。传统模式中,直接将P右移1位,也就是将P的首字符'a'去和目标串的'b'字符进行检测,这明显是多余的。通过我们肉眼的观察,可以很简单的知道应该将模式串P右移到下图'a3'处再开始新一轮的检测,直接跳过肯定不匹配的字符'b',那么我们“肉眼”观察的这一结果怎么把它用语言表示出来呢?
 
 

     我们的观察过程是这样的:
          P的前缀"ab"中'a' != 'b',又因该前缀已经匹配了T中对应的"ab",因此,该前缀的字符'a1'肯定不会和T中对应的字串"ab"中的'b'匹配,也就是将P向右滑动一个位移是无意义的。
          接下来考察P的前缀"aba",发现该前缀自身的前缀'a1'与自身后缀'a2'相等,"a1 b a2" 已经匹配了T中的"a b a3",因此有 'a2' == 'a3', 故得到 'a1' == 'a3'......
          利用此思想,可推知在已经匹配 q=5 个字符的情况下,将P向右移 当且仅当 2个位移时,才能满足既没有冗余(如把'a'去和'b'比较),又不会丢失(如把'a1' 直接与 'a4' 开始比较,则丢失了与'a3'的比较)。
          而前缀函数就是这样一种函数,它决定了q与位移的一一对应关系,通过它就可以间接地求得位移s。
   
     通过对各种模式串进行上述分析(大家可以自己多写几个模式串出来自己分析理解),发现给定一个匹配字符数 q ,则唯一对应一个有效位移,如上述q=5,则对应位移为2.
     这就形成了一一对应关系,而这种唯一的关系就是由前缀函数决定的。
     这到底是怎样的一种关系呢?
     通过对诸多模式串实例的研究,我们会找到一个规律(规律的证明及引理详见《算法导论(第二版)》)。
     上例中,P 已经匹配的字符串为"ababa",那么这个字符串中,满足既是自身真后缀(即不等于自身的后缀),又是自身最长前缀的字符串为"aba",我们设这个特殊字串的长度为L,显然,L = 3. 故我们要求的 s = q - L = 5 - 3 = 2 ,满足前述分析。
   
     根据这个规律,即可得到我们要求的有效位移s,等于已经匹配的字符数 q 减去长度 L。
     即 s = q - L
     因为这个长度 L 与 q 一一对应,决定于q,因此用一函数来表达这一关系非常恰当,这就是所谓的前缀函数了。
     因为已经分析得到该关系为一一对应关系,因此用数组来表示该函数是比较恰当的,以数组的下标表示已经匹配的字符数 q,以下标对应的数据存储 L。

了解了前缀函数原理,也就是说我们可以计算出位移量,那么后续移位的形式就和单步一样了。

所以KMP算法的关键在于,确定next的位置.

next的确定就是找到已经匹配的字符串既是自身真后缀,又是自身最长前缀的字符串长度 用总长度减之,就是我们指针要挪的next位了。

所以问题转化为,找一个字符串中,相等的最长前缀与自身真后缀。和用next位再次对比的事情。

实现代码如下:

public class KMP {
        public static void main(String args[]){
              String str1= "abcdeababcdqqabcdeabc";
              String str2= "abcdeabc";
              String str3="abcd";
              System. out.println(strIn(str1,str2));
              System. out.println(kmp(str1,str2));
       }
       
        public static int strIn(String str1,String str2){
               char[] a=str1.toCharArray();
               char[] b=str2.toCharArray();
               int i=0;
               int j=0;
               while(i<a.length &&j<b.length){
                      if(a[i]==b[j]){
                           i++;
                           j++;
                     } else{
                           i=i-j+1;
                           j=0;
                     }
              } if(j==b.length ){
                      return i-j;
              }
              
               return -1;
              
       }
       
        public static int kmp(String str1,String str2){
               char[] a=str1.toCharArray();
               char[] b=str2.toCharArray();
               int[] next=getNext(b);
               int j=0;
               for(int i=0;i<a.length;i++){
                      //当找到第一个匹配字符时,即j>0,且后续不匹配,才会执行这个循环,去找合适的回退位置
                      while(j>0&&b[j]!=a[i]){
                            //j退回合适的回退位置
                           j=next[j-1];
                     }
                      //如果匹配,j++
                      if(b[j]==a[i]){
                           j++;
                     }
                      if(j==b.length ){
                            //return i-j;
                           System. out.println(i-j);
                            //j=next[j-1];
                            return i-j;
                     }
                     
              }
               return -1;
              
       }
       
        public static int[] getNext(char[] b){
               int next[]=new int[b.length];
              next[0]=0;
               int j=0;
               //为何i=1?对比
               for(int i=1;i<b.length;i++){
                      //第一个匹配之后
                      while(j>0&&b[j]!=b[i]){
                           j=next[j];
                     }
                      if(b[j]==b[i]){
                           j++;
                     }
                     next[i]=j;
              }
               return next;
       }
}


 

 

public class KMP {

    /**
     * 对子串加以预处理,从而找到匹配失败时子串回退的位置 找到匹配失败时的最合适的回退位置,而不是回退到子串的第一个字符,即可提高查找的效率
     * 因此为了找到这个合适的位置,先对子串预处理,从而得到一个回退位置的数组
     * @param B,待查找子串的char数组
     * @return
     */

    public static int[] preProcess(char[] b) {
      int size = b.length;
       int[] P = new int[size];
      P[0] = 0;
      int j = 0;
      // 每循环一次,就会找到一个回退位置
      for (int i = 1; i < size; i++) {
          // 当找到第一个匹配的字符时,即j>0时才会执行这个循环
          // 或者说p2中的j++会在p1之前执行(限于第一次执行的条件下)
          // p1
          while (j > 0 && b[j] != b[i]) {
             j = P[j];
          }
          // p2,由此可以看出,只有当子串中含有重复字符时,回退的位置才会被优化
          if (b[j] == b[i]) {
              j++;
          }
          // 找到一个回退位置j,把其放入P[i]中
          P[i] = j;
      }
      return P;
    }

 

    /**
     * KMP实现
     *
     * @param parStr
     * @param subStr
     * @return
     */

    public static void kmp(String parStr, String subStr) {
      int subSize = subStr.length();
      int parSize = parStr.length();
      char[] b = subStr.toCharArray();
      char[] a = parStr.toCharArray();
      int[] P = preProcess(b);
      int j = 0;
      int k = 0;
      for (int i = 0; i < parSize; i++) {
          // 当找到第一个匹配的字符时,即j>0时才会执行这个循环
          // 或者说p2中的j++会在p1之前执行(限于第一次执行的条件下)
          // p1
          while (j > 0 && b[j] != a[i]) {
              // 找到合适的回退位置
              j = P[j - 1];
          }
          // p2 找到一个匹配的字符
          if (b[j] == a[i]) {
              j++;
          }
          // 输出匹配结果,并且让比较继续下去
          if (j == subSize) {
            j = P[j - 1];
              k++;
              System.out.printf("Find subString '%s' at %d\n", subStr, i
                   - subSize + 1);
          }
      }
      System.out.printf("Totally found %d times for '%s'.\n\n", k, subStr);
    }

 

    public static void main(String[] args) {
      // 回退位置数组为P[0, 0, 0, 0, 0, 0]
     // kmp("abcdeg, abcdeh, abcdef!这个会匹配1次", "abcdef");
      // 回退位置数组为P[0, 0, 1, 2, 3, 4]
      kmp("Test ititi ititit! Test ititit!这个会匹配2次", "ititit");
      // 回退位置数组为P[0, 0, 0]
      kmp("测试汉字的匹配,张三。这个会匹配1次", "张三");
      // 回退位置数组为P[0, 0, 0, 1, 2, 3, 4, 5, 6]
      kmp("这个会匹配0次", "it1it1it1");
    }

}


-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

/**
 * KMP算法的实现
 * @author dfeng
 *
 */
public class KMPAlgorithm {

	/**
	 * 判断是否匹配
	 * @param target 目标文本串
	 * @param mode 模式串
	 * @return 匹配结果
	 */
	public static int matchString(String target, String mode) {
		//为了和算法保持一致,使index从1开始,增加一前缀
		String newTarget = "x" + target; 
		String newMode = 'x' + mode;
		
		int[] K = calculateK(mode);
		
		int i = 1;
		int j = 1;
		while(i <= target.length() && j <= mode.length()) {
			if (j == 0 || newTarget.charAt(i) == newMode.charAt(j)) {
				i++;
				j++;
			} else {
				j = K[j];
			}
		}
		
		if (j > mode.length()) {
			return j;
		}
		return -1;
	}
	
	/*
	 * 计算K值
	 */
	private static int[] calculateK(String mode) {
		//为了和算法保持一致,使index从1开始,增加一前缀
		String newMode = "x" + mode;
		int[] K = new int[newMode.length()];
		int i = 1;
		K[1] = 0;
		int j = 0;
		
		while(i < mode.length()) {
			if (j == 0 || newMode.charAt(i) == newMode.charAt(j)){
				i++;
				j++;
				K[i] = j;
			} else {
 				j = K[j];
			}
		}
		
		return K;
	}
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		String a = "bcabcabcabbcabcabcabcab";
		String b = "bcabcabcabc";//"ababbaaba";//
		System.out.println(KMPAlgorithm.matchString(a, b));

	}

}




---------------------------------------------------------------------------------------------------------------------------------------------

参考资料:

http://www.cnblogs.com/yjiyjige/p/3263858.html

http://billhoo.blog.51cto.com/2337751/411486

http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html



1.2、题目描述

在一个字符串中找到第一个只出现一次的字符。如输入abaccdeff,则输出b。 

代码则可以如下编写:

public class test01 {
     //此方法用到了indexOf(char,startIndex)
     public static void main(String[] args){
          String string="abcdace";
          for(int i=0;i<string.length();i++){
               char c = string.charAt(i);
               if(string.indexOf(c, i+1) == -1){
                    System.out.println(c);
                    break;
               }              
          }
     }
}
---------------------------------------------------------------------------------------
public class test01 {
     //此处用到indexOf和lastIndexOf
     public static void main(String[] args){
          String string="abcdace";
          for(int i=0;i<string.length();i++){
               char c = string.charAt(i);
System.out.println(string.indexOf(c));
System.out.println(string.lastIndexOf(c));
               if(string.indexOf(c) == string.lastIndexOf(c)){
                    System.out.println(c);
                    break;
               }
          }
     }
}



找出现多次的字符
-------------------------------------------------------------------------------------
public class test01 {
    
     public static void main(String[] args){
          String string="abcdace";
          for(int i=0;i<string.length();i++){
               char c = string.charAt(i);
               if(string.indexOf(c, i+1) != -1){
                    System.out.println(c);
                    break;
               }
          }
     }
}
-------------------------------------------------------------------------------------
public class test01 {
     //此处用到indexOf和lastIndexOf
     public static void main(String[] args){
          String string="abcdace";
          for(int i=0;i<string.length();i++){
               char c = string.charAt(i);
               if(string.indexOf(c) != string.lastIndexOf(c)){
                    System.out.println(c);
                    break;
               }
          }
     }
}

------------------------------------------------------------------------------------
需改进的hashmap方法

import java.util.HashMap;
import java.util.Scanner;


public class StringFirst {
public static void main(String args[]){
       Scanner scanner=new Scanner(System. in);
       System. out.print("请输入字符串:" );
       String str=scanner.nextLine();
       System. out.print(str);
        char []strArray=str.toCharArray();
       HashMap<Character,Integer> map= new HashMap<Character,Integer>();
        for(int i=0;i<strArray.length;i++){
               if(map.containsKey(strArray[i]))
              {
                     map.put(strArray[i],map.get(strArray[i])+1);
              }
        else{
              map.put(strArray[i], 1);
       }}
       
       System. out.println(map);
       
        for(int i=0;i<strArray.length;i++){
               if(map.get(strArray[i])==1){
                     
                      
                     System. out.print("第一个出现一次的字符是" +strArray[i]);
                      break;
              }
              
       }
       
}
}

第二节、字符串拷贝
题目描述:
要求实现库函数strcpy,
原型声明:extern char *strcpy(char *dest,char *src); 
功能:把src所指由NULL结束的字符串复制到dest所指的数组中。  
说明:src和dest所指内存区域不可以重叠且dest必须有足够的空间来容纳src的字符串。  
返回指向dest的指针。

这个有点迷糊,不太理解此题意义。。因为实在是简单,是否题目理解错误。

public class StrCopy {
	public static void main(String args[]){
		String str="ashdkiwekfsgmnklrger";
		
		String str1=strCopy(str);
		System.out.println(str1);
	}
	
	public static String strCopy(String str){
		char[] a=str.toCharArray();
		char[] b=new char[a.length];
		for(int i=0;i<a.length;i++){
			b[i]=a[i];
		}
		return new String(b);
	}
}


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值