数据结构与算法--第一个只出现一次的字符

第一个只出现一次的字符
  • 题目:在字符串中找出第一个只出现一次的字符,比如输入“wersdfxvsdfwer”,则输出x。

  • 方法一:

    • 还是老规矩,初始想法是从头遍历每一个字符,每遍历一个字符都与后面n-1个字符比较
    • 如果发现后面字符中包含相同字符则查询下一个字符
    • 如果后面字符都没有相同的字符,则放回当前字符
    • 方法的时间复杂度是O(n2)
  • 方法一是最简单的方式,时间复杂度最大。接下来开始优化改这个算法

  • 方法二:

    • 与次数相关,我们可以将每个字符与出现的次数存储起来,需求开辟额外的存储空间
    • 显然key,value形式的存储第一个想到的是java中的Map结构
    • 通过Map,我们可以通过字符串key,来找到他对应的次数value
    • 有如下实现:
/**
 * 找出数组中第一个只出现一次的字符 HashMap
 * @author liaojiamin
 * @Date:Created in 10:22 2021/6/10
 */
public class FindNotRepeatChar {

public static void main(String[] args) {
        System.out.println(findNotRepeatCharHashMap("wersdfxvsdfwer"));
}

/**
     * HashMap情况
     * */
     public static char findNotRepeatCharHashMap(String str){
        if(str == null){
            return '\u0000';
        }
        if(str.length() == 1){
            return str.charAt(0);
        }
        Map<Character, Integer> map = new HashMap<>();
        char[] target = str.toCharArray();
        for (int i = 0; i < target.length; i++) {
            Character key = target[i];
            if(map.containsKey(key)){
                map.put(key, map.get(key) + 1);
            }else {
                map.put(key, 1);
            }
        }
        for (int i = 0; i < target.length; i++) {
            if(map.get(target[i]) == 1){
                return target[i];
            }
        }
        return '\u0000';
    }

}

  • 如上算法的实现是没问题的,得到的第一个是x,但是此时我们是借助了java的HashMap的api来实现我们的算法

  • 如果只能用基础数据结构以及自己实现的方法呢(如此变态的要求),我们接着来优化。

  • 方法三:

    • 在方法二的基础上我们其实只需要解决HashMap存在的顺序问题,能否自定义一个哈希表
    • 因为题目的特殊性,我们需要将先出现的字符放在某个数据结构的前面
    • 那么自定义一个哈希表,直接通过hashCode来指定对应的位置
    • 因为字符char,在C++中是1个字节,8位 28 = 256,在java中是2个字节,16位,65535
    • 那么我们创建一个长度为65535的数组,每个字母根据其ASCII码作为数组下标对应的一个数字,二数组中存储的是每个字符出现的次数
    • 这样我们创建了一个大小为65535,以字符ASCII码作为键值的哈希表
    • 还有一个小的关键点在于,字符的ASCII编码可以直接通过Integer.valueOf得到,恰好,Character的HashCode方法也是直接转int,他是是相等的
    • 如上代码有如下实现:
/**
 * 找出数组中第一个只出现一次的字符
 * @author liaojiamin
 * @Date:Created in 10:22 2021/6/10
 */
public class FindNotRepeatChar {
    public static void main(String[] args) {
        System.out.println(findNotRepeatChar("wersdfxvsdfwer"));
    }
	
	/**
     * 自定义Hash表方法
     * */
    public static char findNotRepeatChar(String str){
        if(str == null){
            return '\u0000';
        }
        if(str.length() == 1){
            return str.charAt(0);
        }

        int[] charValue = new int[256];
        char[] target = str.toCharArray();
        for (int i = 0; i < target.length; i++) {
            Integer position = new Character(target[i]).hashCode();
            charValue[position] += 1;
        }
        for (int i = 0; i < target.length; i++) {
            Integer position = new Character(target[i]).hashCode();
            if(charValue[position] == 1){
                return target[i];
            }
        }
        return '\u0000';
    }
}

  • 以上代码能正确得到我们需要的结果,并且第一次遍历,在哈希表中更新一个字符出现的次数时间是O(1)。如果字符串长度为n,那么一次扫描时间复杂度是O(n)

  • 第二次遍历得到的Hash表同样可以O(1)时间复杂度得到一个字符的次数,所以时间复杂度依然是O(1)

  • 这样总的来说时间复杂度还是O(1)

  • 同时我们需要一个 65535 的整数数组,由于数组的大小是个常数,也就是数组大小不会和目标字符串的长度相关,那么空间复杂度是O(1)

  • 问题:

    • 在方法三种,的确可以在纯英文字符的情况下得到确定值,算法也是正确的,但是实际上工作中字符远比65535个多,而且hashCode也会有冲突的时候,比如***存在中英文混合情况,方法二就无法求解***
  • 方法四:

    • 基于方法二的基础上我们解决中英文混用造成的冲突以及顺序问题
    • 我想到了HashMap中用到的哈希冲突解决方法,分离链表发
    • 我们定义一个链表数组,每次冲突后,将冲突元素添加到链表尾部
    • 然后依次遍历找出为 1 的节点即可
    • 如下实现:
/**
 * 找出数组中第一个只出现一次的字符
 * @author liaojiamin
 * @Date:Created in 10:22 2021/6/10
 */
public class FindNotRepeatChar {
    public static void main(String[] args) {
        System.out.println(findNotRepeatCharCompatibleChina("wersdfxxv我sdfwer"));
    }
/**
     * 异常情况:无法保证中文,英文数据在数组中顺序
     * 包含中文情况
     * */
    public static char findNotRepeatCharCompatibleChina(String str){
        if(str == null){
            return '\u0000';
        }
        if(str.length() == 1){
            return str.charAt(0);
        }
        ListNode listArray[] = new ListNode[str.length()];
        char[] target = str.toCharArray();
        for (int i = 0; i < target.length; i++) {
            Integer position = (new Character(target[i]).hashCode())%str.length();
            if(listArray[position] == null){
                listArray[position] = new ListNode(String.valueOf(target[i]), 1);
            }else {
                ListNode listNode = MyLinkedList.search(listArray[position], String.valueOf(target[i]));
                if(listNode != null){
                    listNode.setValue(listNode.getValue()+1);
                }else {
                    MyLinkedList.addToTail(listArray[position], String.valueOf(target[i]), 1);
                }
            }
        }
        for (int i = 0; i < listArray.length; i++) {
            if(listArray[i] != null){
                ListNode header = listArray[i];
                while (header != null){
                    if(header.getValue() == 1){
                        return header.getKey().charAt(0);
                    }
                    header = header.getNext();
                }
            }
        }
        return '\u0000';
    }
 }
  • 如上,算法参照HashMap的思想对hash表进行处理,算法能得到正确的值。

  • 我们试图通过一个 字符串大小的链表数组来存储对应存量数据,其中涉及到HashCode%str.length取模得到对应位置

  • 虽然获取数组位置的时候回有冲突,并且不能保证顺序,但是我们每次都通过原始数组去查找遍历,依然可以得到第一个出现一次的字符

  • 方法中用的链表ListNode,以及链表对应的方法 MyLinkedList 都是自定义的方法,可以在之前的文章:数据结构与算法–链表实现以及应用 找到详细的实现以及说明。

  • 方法的时间时间复杂度两次遍历都是O(n)

  • 在每次遍历有hash冲突的节点时候,我们需要调用 MyLinkedList.search 找到当前key值对应的节点,此处复杂度取决于hash冲突的多少

  • 因此时间复杂度应该大于O(n)

  • 空间复杂度额外存储于字符串长度正相关也是O(n)

  • 方法五

    • 在方法四中算法是正确,但是有一定的复杂度,涉及到hash冲突解决,取模定位,链表节点查询这种复杂的操作
    • 因为我们收到方法三的定式思维影响用的hash表的结构存储,其实完全不用
    • 我们可以用链表存储,不用hash作为key,直接用对应的字符作为key,这样可以用少于O(n)的空间来存储
    • 如上分析有如下实现:
/**
 * 找出数组中第一个只出现一次的字符
 * @author liaojiamin
 * @Date:Created in 10:22 2021/6/10
 */
public class FindNotRepeatChar {
    public static void main(String[] args) {
        System.out.println(findNotRepeatCharCompatibleChinaLinkList("哈哈wersvdfxx我v我sdfwer去"));
    }
 /**
     * 用链表解决
     * */
    public static char findNotRepeatCharCompatibleChinaLinkList(String str) {
        if (str == null) {
            return '\u0000';
        }
        if (str.length() == 1) {
            return str.charAt(0);
        }
        //初始化链表
        ListNode listNode  = new ListNode(String.valueOf(str.charAt(0)), 1);
        char[] target = str.toCharArray();
        for (int i = 1; i < target.length; i++) {
            ListNode header = MyLinkedList.search(listNode, String.valueOf(target[i]));
            if(header != null){
                header.setValue(header.getValue() + 1);
            }else {
                MyLinkedList.addToTail(listNode, String.valueOf(target[i]), 1);
            }
        }
        for (int i = 0; i < target.length; i++) {
            ListNode targetNode = MyLinkedList.search(listNode, String.valueOf(target[i]));
            if(targetNode != null && targetNode.getValue() == 1){
                return target[i];
            }

        }
        return '\u0000';
    }
}
  • 如上用链表存储的实现方式时间复杂度与之前一样,也是大于O(n),但是在代码复杂度上减少很多
  • 此方法不涉及到hash冲突解决等问题,都是直接存储,空间复杂度是小于O(n)的

上一篇:数据结构与算法–丑数
下一篇:数据结构与算法–数组中的逆序对

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值