数据结构---数组(4)

1、把数组排成最小的数(剑指offer--33)

问题描述:输入一个正整数数组,将它们连接起来排成一个数,输出能排出的所有数字中最小的一个。例如输入数组{32,  321},则输出这两个能排成的最小数字32132。请给出解决问题的算法,并证明该算法。

思路:先将整数数组转为字符串数组,然后字符串数组进行排序,最后依次输出字符串数组即可。这里注意的是字符串的比较函数需要重新定义,不是比较a和b,而是比较ab与 ba。

如果ab < ba,则a < b;

如果ab > ba,则a > b;

如果ab = ba,则a = b。

比较函数的定义是本解决方案的关键。

     

证明:为什么这样排个序就可以了呢?简单证明一下。根据算法,如果a < b,那么a排在b前面,否则b排在a前面

可利用反证法,假设排成的最小数字为xxxxxx,并且至少存在一对字符串满足这个关系:假设a > b,在组成的数字中a排在b前面。

根据a和b出现的位置,分三种情况考虑:

(1)xxxxab,用ba代替ab可以得到xxxxba,这个数字是小于xxxxab,与假设矛盾。因此排成的最小数字中,不存在上述假设的关系。
(2)abxxxx,用ba代替ab可以得到baxxxx,这个数字是小于abxxxx,与假设矛盾。因此排成的最小数字中,不存在上述假设的关系。

(3)axxxxb,这一步证明麻烦了一点。可以将中间部分看成一个整体ayb,则有ay < ya,yb < by成立。

将ay和by表示成10进制数字形式,则有下述关系式,这里a,y,b的位数分别为n,m,k。

关系1: ay < ya => a * 10^m + y < y * 10^n + a => a * 10^m - a < y * 10^n - y => a( 10^m - 1)/( 10^n - 1) < y
关系2: yb < by => y * 10^k + b < b * 10^m + y => y * 10^k - y < b * 10^m - b => y < b( 10^m -1)/( 10^k -1) 

关系3: a( 10^m - 1)/( 10^n - 1) < y < b( 10^m -1)/( 10^k -1)  => a/( 10^n - 1)< b/( 10^k -1) => a*10^k - a < b * 10^n - b =>a*10^k + b < b * 10^n + a => a < b

这与假设a > b矛盾。因此排成的最小数字中,不存在上述假设的关系。

综上所述,得出假设不成立,从而得出结论:对于排成的最小数字,不存在满足下述关系的一对字符串:a > b,但是在组成的数字中a出现在b的前面。从而得出算法是正确的。

	//重新定义比较规则
	public static int compare(String a, String b) {
		String ab = a + b;
		String ba = b + a;
		return ab.compareTo(ba);
	}

	// 打印最小值
	public static void printMinNuumber(int arr[], int length) {

		if (arr == null || length <= 0)
			return;
		
		String num[] = new String[length];

		for (int i = 0; i < length; i++) {
			num[i] = arr[i] + "";
		}

		boolean flag = true;
		// 排序数组---冒泡
		for (int i = 0; i < length-1&& flag ; i++) {
			flag = false;
			for (int j = length - 1; j > i; j--) {
				if (compare(num[j], num[j - 1]) < 0) {
					String temp = num[j];
					num[j] = num[j - 1];
					num[j - 1] = temp;
					flag = true;
				}
			}
		}
		
		//打印数组
		for (int i = 0; i < length; i++) {
			System.out.print(num[i]);
		}
	}
2、丑数(剑指offer--34)

我们把只包含因子2、3和5的数称作丑数。例如6、8都是丑数,但14不是,因为它包含因子7。习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第1500个丑数。

解法一:

	//判断是否是丑数
	public static boolean IsUgly(int number)
	{
	    while(number % 2 == 0)
	        number /= 2;
	    while(number % 3 == 0)
	        number /= 3;
	    while(number % 5 == 0)
	        number /= 5;
	    return (number == 1) ? true : false;
	}
	//找第index个丑数
	public static int getUglyNum(int index)
	{
	    if(index <= 0)
	        return 0;
	  
	    int number = 0;
	    int uglyFound = 0;
	    while(uglyFound < index)
	    {
	        ++number;
	  
	        if(IsUgly(number))
	        {
	            ++uglyFound;
	        }
	    }
	  
	    return number;
	}
上面的方法,效率低的无法让人接受。于是,我们在寻求更好的解决办法。仔细阅读题目,会发现这题貌似有点像找素数的问题,对了,就是这样,我们换个思路,不是去枚举所有符合条件的数,而是去通过条件生成这些数字。根据丑数的定义,丑数应该是另一个丑数乘以2、3或者5的结果(1除外)。因此我们可以创建一个数组,里面的数字是排好序的丑数。里面的每一个丑数是前面的丑数乘以2、3或者5得到的。这个思路的关键点,就是要保证数组里面的丑数是排好序的。假设arr[1..i]是已经排好序的数组,则arr[i]一定是这里面最大的数,那么我们只要去寻找新生成的数字中比arr[i]大的的最小的数。新生成的数是由前面的数字*2或*3或*5得到的。我们定义index2为前面数字*2中的所有数字中满足大于arr[i]的最小的数的下标,index3,index5类似定义,则应该放在arr[i+1]位置的数字便是min(arr[index2]*2,arr[index3]*3,arr[index5]*5)。

注意代码里,index2,index3,index5是维持动态向前的,不会产生无效搜索,因为当前找的数字一定比原来找的要大,所以从上一次找到的下标开始进行搜索就可以了。
具体代码实现如下:

	public static int Min(int a, int b, int c) {
		a = a < b ? a : b;
		if (c < a)
			return c;
		else
			return a;
	}

	public static int getUglyNum1(int Mindex) {
		int index = 1;
		int[] arr = new int[Mindex];
		arr[0] = 1;
		int index2 = 0, index3 = 0, index5 = 0;
		while (index < Mindex) {
			
			int min = Min(arr[index2] * 2, arr[index3] * 3, arr[index5] * 5);			
			arr[index] = min;
			
			while (arr[index2] * 2 <= arr[index])
				index2++;
			while (arr[index3] * 3 <= arr[index])
				index3++;
			while (arr[index5] * 5 <= arr[index])
				index5++;
			index++;
		}

		int ans = arr[Mindex - 1];

		return ans;
	}

3、第一个只出现一次的字符(剑指offer--35)

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

	// O(n^n)的时间复杂度
	public static char FirstNotRepeatingChar1(char[] pString) {
		// 如果是空指针,返回\0
		if (pString == null)
			return '\0';

		int len = pString.length;
		for (int i = 0; i < len; i++) {
			int flag = 0; // 标识位,0表示这个字符只出现一次。
			for (int j = 0; j < len; j++) {
				if (pString[i] == pString[j] && i != j) {
					flag = 1; // 1表示在当前字符后面存在于该字符相同的字符。
				}
			}

			if (flag == 0)
				return pString[i];
		}
		return '\0';
	}

	// O(n)的时间复杂度
	public static char FirstNotRepeatingChar(char[] pString) {
		// 如果是空指针,返回\0
		if (pString == null)
			return '\0';

		// 定义hash表长度256,并创建哈希表
		int len = 256;
		int hashtable[] = new int[len];

		for (int i = 0; i < len; i++) {
			hashtable[i] = 0;
		}

		// 第一遍遍历字符串,求出每个字符出现的次数
		for (int i = 0; i < pString.length; i++) {
			hashtable[pString[i]]++;
		}

		// 第二遍遍历字符串,求出第一个只出现一次的字符,每次都是按照字符串的顺序遍历
		for (int i = 0; i < pString.length; i++) {
			if (hashtable[pString[i]] == 1)
				return pString[i];
		}

		return '\0';
	}
4、数组中的逆序对(剑指offer--36)
题目:
在数组中的两个数字如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。例如,有一个数组为Array[0..n] 其中有元素a[i],a[j].如果 当i<j时,a[i]>a[j],那么我们就称(a[i],a[j])为一个逆序对。在数组{7,5,6,4}中一共存在5对逆序对,分别是(7,6),(7,5),(7,4),(6,4),(5,4)。

解法一:

	//逆序对
	public static int CountInversions(int [] a)
	{
	    int count = 0;
	    for (int i = 0; i < a.length; i++)
	    {
	        for (int j = i + 1; j < a.length; j++)
	        {
	            if (a[i] > a[j]) count++;
	        }
	    }
	    return count;
	}
解法二:

考虑一下,逆序是说a[i]>a[j],i<j。那么在排序的过程中,会把a[i]和a[j]交换过来,这个交换的过程,每交换一次,就是一个逆序对的“正序”过程。
一个比较好的思路是利用分治的思想:先求前面一半数组的逆序数,再求后面一半数组的逆序数,然后求前面一半数组比后面一半数组中大的数的个数(也就是逆序数),这三个过程加起来就是整体的逆序数目了。看这个描述,是不是有点像归并排序呢?归并排序的思想就是把前一段排序,后一段排序,然后再整体排序。而且,归并排序的规程中,需要判断前一半数组和后一半数组中当前数字的大小。这也就是刚刚描述的逆序的判断过程了。如果前一半数组的当前数字大于后一半数组的当前数字,那么这就是一个逆序数。
利用归并排序的过程中,在每一次归并两个数组的时候,如果左数组比右数组大,那么着就是一个逆序。记录所有左数组比右数组大的情况,就是全部的逆序数目。

   public static int count = 0;  
	
    public static void mergeSort(int[] data) {  
        sort(data, 0, data.length - 1);  
    }  
      
    public static void sort(int[] data, int left, int right) {  
        if (left >= right)  
            return;       
        int center = (left + right) / 2;// 找出中间索引         
        sort(data, left, center);// 对左边数组进行递归         
        sort(data, center + 1, right);// 对右边数组进行递归    
        merge(data, left, center, right);// 合并  
    }  
      
    public static void merge(int[] data, int left, int center, int right) {  
        int[] tmpArr = new int[data.length];  
        int mid = center + 1;  
        int third = left;  
        int tmp = left;  
        while (left <= center && mid <= right) {  
            if (data[left] <= data[mid]) {  
                tmpArr[third++] = data[left++];  
            } else {  
                tmpArr[third++] = data[mid++]; 
                // 因为如果a[left]此时比右数组的当前元素a[mid]大,  
                // 那么左数组中a[left]后面的元素就都比a[mid]大  
                // 【因为数组此时是有序数组】  
                count += center - left + 1; 
            }  
        }  
        while (mid <= right) {  
            tmpArr[third++] = data[mid++];  
        }  
        while (left <= center) {  
            tmpArr[third++] = data[left++];  
        }  
      
        // 将临时数组中的内容拷贝回原数组中  
        while (tmp <= right) {  
            data[tmp] = tmpArr[tmp++];  
        }  
    }  



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值