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++];
}
}