算法学习系列(8)—— 前缀树以及贪心算法、中位数问题

1.介绍前缀树

[问题] 何为前缀树? 如何生成前缀树?

1.1 何为前缀树:

直接举例说明:
先给定一个字符串:abc,建立一棵树,从头结点有走向a的路不?没有就见一个节点,a加在该路径上,然后b也加在后边,c继续。当再给一个字符串be的时候,从头节点开始没有通向b的路,所以重新建节点,再建路,bce添加上去。然后对于abd,有了ab,在后边加d就行。bef也是一样的道理。最终建立的这棵树就叫做前缀树(Trie[try同音]树)。
在这里插入图片描述

1.2 前缀树相关的问题

 1)arr2中有哪些字符,是arr1中出现的?请打印
 2)arr2中有哪些字符,是作为arr1中某个字符串前缀出现的?请打印
 3)arr2中有哪些字符,是作为arr1中某个字符串前缀出现的?请打印 arr2中出现次数最大的前缀。
public class Code_01_TrieTree {

	public static class TrieNode {
		public int path;//有多少个节点到达过
		public int end;//有多少个字符串结尾的
		public TrieNode[] nexts;//路

		public TrieNode() {
			path = 0;
			end = 0;
			nexts = new TrieNode[26];//每个节点都默认有26条路,a-z
		}
	}

	public static class Trie {
		private TrieNode root;

		public Trie() {
			root = new TrieNode();
		}

		public void insert(String word) {
			if (word == null) {
				return;
			}
			char[] chs = word.toCharArray();
			TrieNode node = root;
			int index = 0;
			for (int i = 0; i < chs.length; i++) {
				index = chs[i] - 'a';
				if (node.nexts[index] == null) {
					node.nexts[index] = new TrieNode();
				}
				node = node.nexts[index];
				node.path++;
			}
			node.end++;
		}

		public void delete(String word) {
			if (search(word) != 0) {
				char[] chs = word.toCharArray();
				TrieNode node = root;
				int index = 0;
				for (int i = 0; i < chs.length; i++) {
					index = chs[i] - 'a';
					if (--node.nexts[index].path == 0) {//如果当前的path已经等于0了,后边的节点就直接指向空就行了
						node.nexts[index] = null;
						return;
					}
					node = node.nexts[index];
				}
				node.end--;
			}
		}

		public int search(String word) {
			if (word == null) {
				return 0;
			}
			char[] chs = word.toCharArray();
			TrieNode node = root;
			int index = 0;
			for (int i = 0; i < chs.length; i++) {
				index = chs[i] - 'a';
				if (node.nexts[index] == null) {
					return 0;
				}
				node = node.nexts[index];
			}
			return node.end;
		}

		public int prefixNumber(String pre) {
			if (pre == null) {
				return 0;
			}
			char[] chs = pre.toCharArray();
			TrieNode node = root;
			int index = 0;
			for (int i = 0; i < chs.length; i++) {
				index = chs[i] - 'a';
				if (node.nexts[index] == null) {
					return 0;
				}
				node = node.nexts[index];
			}
			return node.path;
		}
	}

	public static void main(String[] args) {
		Trie trie = new Trie();
		System.out.println(trie.search("zuo"));
		trie.insert("zuo");
		System.out.println(trie.search("zuo"));
		trie.delete("zuo");
		System.out.println(trie.search("zuo"));
		trie.insert("zuo");
		trie.insert("zuo");
		trie.delete("zuo");
		System.out.println(trie.search("zuo"));
		trie.delete("zuo");
		System.out.println(trie.search("zuo"));
		trie.insert("zuoa");
		trie.insert("zuoac");
		trie.insert("zuoab");
		trie.insert("zuoad");
		trie.delete("zuoa");
		System.out.println(trie.search("zuoa"));
		System.out.println(trie.prefixNumber("zuo"));

	}

}

2.贪心策略

不要想着证明贪心策略是对的不,直接使用对数器去验证就好。

2.1分金条问题

【题目】:
一块金条切成两半,是需要花费和长度数值一样的铜板的。比如 长度为20的金条,不管切成长度多大的两半,都要花费20个铜 板。一群人想整分整块金条,怎么分最省铜板?
例如,给定数组{10,20,30},代表一共三个人,整块金条长度为 10+20+30=60. 金条要分成10,20,30三个部分。 如果,先把长 度60的金条分成10和50,花费60 再把长度50的金条分成20和30, 花费50 一共花费110铜板。 但是如果, 先把长度60的金条分成30和30,花费60 再把长度30 金条分成10和20,花费30 一共花费90铜板。
【要求】:输入一个数组,返回分割的最小代价。即{10,20,30} ==> 90
【解题】:
这个题实际上是哈夫曼编码问题,想把金条断成规定的多少段,选择一个怎样的顺序能让代价最低。一共是10,20,30这三个,10和20合成一个30,30和30合成一个60,共需要代价90 。可以认为每一块是一个叶节点,怎么决定叶节点的合并顺序让整体的合并代价最低。合并代价怎么评估?两个叶节点合并之后产生的和就是它的合并代价。相当于这个题是求所有非叶节点的值加起来谁低。这个题整体就转化为:给了叶节点,选择一个什么合并顺序,能够导致非叶节点整体的求和最小。

具体做法是贪心,怎么证明先忽略,这里主要说怎么用堆来完成这个代价。用小根堆结构,假设有{10,20,30},先把它组成小根堆,一次从堆里弹出俩最小的,10和20,则小根堆里面还剩下30,10和20组成一个30节点,产生代价30,然后把30节点扔回堆里,然后再从堆里弹出俩最小的,合并完的节点再扔回堆里。如果有多个数的话,所有叶子节点组成一个小根堆,小根堆里一次弹出俩合成一个节点 ,扔回堆里。中途产生的代价依次累加,就是最小代价。

public class Code_02_GetLessMoney {
    public static int lessMoney(int[] arr){
        PriorityQueue<Integer> pQ = new PriorityQueue<>();//优先队列的默认都是小根堆
		for (int i = 0; i < arr.length; i++) {
			pQ.add(arr[i]);//组成一个小根堆
		}
		int sum = 0;//记录最终的结果
		int cur = 0;//记录当前的值
		while (pQ.size() > 1) {
			cur = pQ.poll() + pQ.poll();//将最小的两个叶节点相加,队列结构,先进的先出,排在前边的先出
			sum += cur;//每次要重构小根堆之前都对返回值的结果统计一遍
			pQ.add(cur);//将当前计算出来的结果添加到小根堆里
		}
		return sum;
    }
	//for test
    public static void main(String[] args) {
        int[] arr = {10,30,20};
        System.out.println(lessMoney(arr));//90
    }
}
  • 来一个小插曲:
    PriorityQueue的使用,直接贴代码吧:
public class Test_PriorityQueue {

    public static class MinHeapComparator implements Comparator<Integer>{
        @Override
        public int compare(Integer o1, Integer o2) {
            return o1 - o2;
        }
    }

    public static class MaxHeapComparator implements Comparator<Integer>{
        @Override
        public int compare(Integer o1, Integer o2) {
            return o2 - o1;
        }
    }

    public static void main(String[] args) {
        int[] arr = {10,30,20};
        //test normalHeap
        PriorityQueue queue_normal = new PriorityQueue();
        for(int i = 0;i < arr.length;i++){
            queue_normal.add(arr[i]);
        }
        while (!queue_normal.isEmpty()){
            System.out.print(queue_normal.poll() + " ");
        }
        System.out.println();
        System.out.println("=========================");
        //Test MinHeapComparator
        PriorityQueue queue_min = new PriorityQueue(new MinHeapComparator());
        for(int i = 0;i < arr.length;i++){
            queue_min.add(arr[i]);
        }
        while (!queue_min.isEmpty()){
            System.out.print(queue_min.poll() + " ");
        }
        System.out.println();
        System.out.println("=========================");

        //test MaxHeapComparator
        PriorityQueue queue_max = new PriorityQueue(new MaxHeapComparator());
        for(int i = 0;i < arr.length;i++){
            queue_max.add(arr[i]);
        }
        while (!queue_max.isEmpty()){
            System.out.print(queue_max.poll() + " ");
        }
    }
}

2.2最小花费,最大收益

输入: 参数1,正数数组costs 参数2,正数数组profits 参数3, 正数k 参数4,正数m costs[i]表示i号项目的花费 profits[i]表示i号项目在扣除花 费之后还能挣到的钱(利润) k表示你不能并行、只能串行的最多 做k个项目 m表示你初始的资金
说明:你每做完一个项目,马上获得的收益,可以支持你去做下一个项目。
输出: 你最后获得的最大钱数。

public class Code_03_IPO {
	public static class Node {
		public int p;//利润
		public int c;//花费

		public Node(int p, int c) {
			this.p = p;
			this.c = c;
		}
	}

	public static class MinCostComparator implements Comparator<Node> {//花费的小根堆

		@Override
		public int compare(Node o1, Node o2) {
			return o1.c - o2.c;
		}

	}

	public static class MaxProfitComparator implements Comparator<Node> {//利润的大根堆

		@Override
		public int compare(Node o1, Node o2) {
			return o2.p - o1.p;
		}

	}

	public static int findMaximizedCapital(int k, int W, int[] Profits, int[] Capital) {
		Node[] nodes = new Node[Profits.length];
		for (int i = 0; i < Profits.length; i++) {//将所有的利润和花费都放进node数组中去
			nodes[i] = new Node(Profits[i], Capital[i]);
		}

		PriorityQueue<Node> minCostQ = new PriorityQueue<>(new MinCostComparator());
		PriorityQueue<Node> maxProfitQ = new PriorityQueue<>(new MaxProfitComparator());
		for (int i = 0; i < nodes.length; i++) {//把花费数据填进去花费数组
			minCostQ.add(nodes[i]);
		}
		for (int i = 0; i < k; i++) {//当做到规定的最大项目数k,就停止做项目
			while (!minCostQ.isEmpty() && minCostQ.peek().c <= W) {//当花费数组中的堆顶元素比规定的本金小,那么就解锁新的项目
				maxProfitQ.add(minCostQ.poll());
			}
			if (maxProfitQ.isEmpty()) {//有可能做不到k个项目就需要停止了
				return W;
			}
			W += maxProfitQ.poll().p;//所有的本金加利润的总和
		}
		return W;
	}

}

2.3 安排时间段内开的会议最多

一些项目要占用一个会议室宣讲,会议室不能同时容纳两个项目 的宣讲。 给你每一个项目开始的时间和结束的时间(给你一个数 组,里面 是一个个具体的项目),你来安排宣讲的日程,要求会 议室进行 的宣讲的场次最多。返回这个最多的宣讲场次。
贪心的策略是:先哪个早开始就安排哪个,后边就是哪个早结束就安排哪个。

public class Code_06_BestArrange {
	public static class Program {
		public int start;
		public int end;

		public Program(int start, int end) {
			this.start = start;
			this.end = end;
		}
	}

	public static class ProgramComparator implements Comparator<Program> {

		@Override
		public int compare(Program o1, Program o2) {
			return o1.end - o2.end;
		}

	}

	public static int bestArrange(Program[] programs, int cur) {
		Arrays.sort(programs, new ProgramComparator());//将所有的项目按照结束时间的早晚先进行排序
		int result = 0;//可以安排的场次
		for (int i = 0; i < programs.length; i++) {
			if (cur <= programs[i].start) {//如果项目开始的时间比当前时间要晚,那么该项目就是可以安排的
				result++;
				cur = programs[i].end;//该项目结束的时候,当前的时间应该转移到该项目的结束时间点上来
			}
		}
		return result;
	}

	public static void main(String[] args) {

	}
}

3.获取数据流中的中位数

【题目】如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。
解题思路:

首先要正确理解此题的含义,数据是从一个数据流中读出来的,因此数据的数目随着时间的变化而增加。对于从数据流中读出来的数据,当然要用一个数据容器来保存,也就是当有新的数据从流中读出时,需要插入数据容器中进行保存。那么我们需要考虑的主要问题就是选用什么样的数据结构来保存。

方法一:用数组保存数据。数组是最简单的数据容器,如果数组没有排序,在其中找中位数可以使用类比快速排序的partition函数,则插入数据需要的时间复杂度是O(1),找中位数需要的复杂度是O(n)。除此之外,我们还可以想到用直接插入排序的思想,在每到来一个数据时,将其插入到合适的位置,这样可以使数组有序,这种方法使得插入数据的时间复杂度变为O(n),因为可能导致n个数移动,而排序的数组找中位数很简单,只需要O(1)的时间复杂度。

方法二:用链表保存数据。用排序的链表保存从流中的数据,每读出一个数据,需要O(n)的时间找到其插入的位置,然后可以定义两个指针指向中间的结点,可以在O(1)的时间内找到中位数,和排序的数组差不多。

方法三:用二叉搜索树保存数据。在二叉搜索树种插入一个数据的时间复杂度是O(logn),为了得到中位数,可以在每个结点增加一个表示子树结点个数的字段,就可以在O(logn)的时间内找到中位数,但是二叉搜索树极度不平衡时,会退化为链表,最差情况仍需要O(n)的复杂度。

方法四:用AVL树保存数据。由于二叉搜索树的退化,我们很自然可以想到用AVL树来克服这个问题,并做一个修改,使平衡因子为左右子树的结点数之差,则这样可以在O(logn)的时间复杂度插入数据,并在O(1)的时间内找到中位数,但是问题在于AVL树的实现比较复杂。

方法五:最大堆和最小堆。我们注意到当数据保存到容器中时,可以分为两部分,左边一部分的数据要比右边一部分的数据小。如下图所示,P1是左边最大的数,P2是右边最小的数,即使左右两部分数据不是有序的,我们也有一个结论就是:左边最大的数小于右边最小的数。
在这里插入图片描述
因此,我们可以有如下的思路:用一个最大堆实现左边的数据存储,用一个最小堆实现右边的数据存储,向堆中插入一个数据的时间是O(logn),而中位数就是堆顶的数据,只需要O(1)的时间就可得到。
  而在具体实现上,首先要保证数据平均分配到两个堆中,两个堆中的数据数目之差不超过1,为了实现平均分配,可以在数据的总数目是偶数时,将数据插入最小堆否则插入最大堆

此外,还要保证所有最大堆中的数据要小于最小堆中的数据。所以,新传入的数据要和最大堆中最大值或者最小堆中的最小值比较。当总数目是偶数时,我们会插入最小堆,但是在这之前,我们需要判断这个数据和最大堆中的最大值哪个更大,如果最大值中的最大值比较大,那么将这个数据插入最大堆,并把最大堆中的最大值弹出插入最小堆。由于最终插入到最小堆的是原最大堆中最大的,所以保证了最小堆中所有的数据都大于最大堆中的数据。

public class Code_04_GetMedian {

    public static int count = 0;
    public static PriorityQueue<Integer> minQueue;
    public static PriorityQueue<Integer> maxQueue;

    public static double getMedian_init(int[] arr){
        minQueue = new PriorityQueue<>();//默认是小根堆
        maxQueue = new PriorityQueue<>(new Comparator<Integer>() {//构造大根堆
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2 - o1;
            }
        });
        for(int i = 0;i < arr.length;i++){
            insert(arr[i]);
        }
        return getMedian(maxQueue,minQueue);
    }

    public static void insert(int num){
        count++;
        if(count % 2 == 0){//偶数,加到最小堆,右边的去
            if(!maxQueue.isEmpty() && maxQueue.peek() > num){//如果左边的最大数比当前数要大,加到左边去
                maxQueue.add(num);
                num = maxQueue.poll();//当前数移到大根堆中的最大的数去
            }
            minQueue.add(num);//左边的小根堆一直都添加较大的数
        }else {
            if(!minQueue.isEmpty() && minQueue.peek() < num){//如果右边的最小数都比当前数小,加到右边
                minQueue.add(num);
                num = minQueue.poll();
            }
            maxQueue.add(num);
        }
    }

    public static double getMedian(PriorityQueue<Integer> maxQueue,PriorityQueue<Integer> minQueue){
        if(maxQueue.size() == minQueue.size()){
            return (maxQueue.peek()+minQueue.peek())/2.0;
        }else if(maxQueue.size() > minQueue.size()){
            return maxQueue.peek()/1.0;
        }else {
            return minQueue.peek()/1.0;
        }
    }

    public static void main(String[] args) {
        int[] arr = {1,4,3,2,5};
        System.out.println(getMedian_init(arr));//3.0
        int[] arr2 = {1,2,3,4,5,6};
        System.out.println(getMedian_init(arr2));//3.5
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值