合並IP地址
問題描述
給定一組IPv4地址,其中每個元素可能由單個IP構成(例如192.168.0.1),也可能由一個IP段構成(如192.168.0.10 - 192.168.0.15),請將給定的IP合並。
輸入例子
{"192.168.0.1",
"192.168.0.12-192.168.0.15",
"192.168.0.2",
"192.168.0.7-192.168.0.9",
"192.168.0.11",
"192.168.0.3-192.168.0.5",
"192.168.0.16",
"192.168.0.100"}
輸出例子
{"192.168.0.1-192.168.0.5",
"192.168.0.7-192.168.0.9",
"192.168.0.11-192.168.0.16",
"192.168.0.100"}
這個問題是基於
但問題是首先需要將IP地址區間轉換為可比較的區間,但注意到Java中Int
類型最大只能表示31位的正數,並不能完整的表示一個32位IP地址,而Java並沒有提供無符號數,所以存儲IP地址,並且用來比較大小,需要用到long
類型,輸出要求是同樣的IP格式,所以涉及到IP地址的String
到long
的相互轉換。
StringToLong
顯而易見的是,32位的IP地址由.
分割為256進制的四個8位的字段,當轉換成數字時,可以從右往左逐乘相加。
public static long ipToLong(String ip) {
String ips[] = ip.split("\\.");
long ipLong = 0;
for (int i = 0; i < 0; i++) {
ipLong = ipLong * 256 + Long.parseLong(ips[i]);
}
return ipLong;
}
但是存在一種效率更高的方法,就是對於每個字段,我們進行移位操作,然后再做一個|
操作,比如對於192.168.19.1
這個地址來說,轉換成二進制即11000000 10101000 00010011 00000001
192是字段的高8位,
我們將IP按.
分割為大小為4的數組后,將第一個數字,左移24位,即得到11000000 00000000 00000000 00000000
,同樣將第二個數字左移16位,得到10101000 00000000 00000000
,以此類推,將第三個數字左移8位得到00010011 00000000
,最低的八位不需要移位,然后將四個結果進行或操作,得到的就是IP地址的十進制形式。具體實現形式如下
public static long ipToLong(String ip) {
String ips[] = ip.split("\\.");
long ipLong = (Long.parseLong(ips[0]) << 24) |
(Long.parseLong(ips[1]) << 16) |
(Long.parseLong(ips[2]) << 8) |
(Long.parseLong(ips[3]));
return ipLong;
}
或者寫成循環
public static long ipToLong(String ip) {
String ips[] = ip.split("\\.");
long ipLong = 0;
for (int i = 0; i < 4; i++) {
ipLong |= Long.parseLong(ips[i]) << (24 - 8 * i);
}
return ipLong;
}
LongToString
這里同樣可以貫徹上面對位運算的思想,依舊使用上面的栗子IP地址192.168.19.1
的數字表示為ip = 3232240385
,我們將long
類型從左往右逐步取8位,對於高8位只需要,將long
類型的IP地址右移24位即可(這里注意一點,如果是32位的int
類型進行轉換時需要使用無符號位移>>>
),即ip>>24
,而要取到從左往右第二個8bit時,我們可以將ip的高8位置零,具體做法0xffffff & ip
,然后右移16位即可,同樣取第三個8bit,可以將高16位置零,然后右移8位即可,最后只需將高24位置零可以得到低8位,然后中間假如分隔符拼接得到String
類型的IP地址,具體做法如下。
public static String ipToString(Long ip) {
StringBuilder sb = new StringBuilder();
sb.append(String.valueOf(ip >> 24));
sb.append(".");
sb.append(String.valueOf((0xffffff & ip) >> 16));
sb.append(".");
sb.append(String.valueOf((0xffff & ip) >> 8));
sb.append(".");
sb.append(String.valueOf(0xff & ip));
return sb.toString();
}
也可以寫成循環
public static String ipToStringCycle(long ip) {
String ipString[] = new String[4];
for (int i = 0; i < 4; i++) {
//取long類型ip地址的后32位
long and = ip & (0xffffffff >>> (i * 8));
//取上面結果的高8位
ipString[i] = String.valueOf(and >> ((3 - i) * 8));
}
return String.join(".", ipString);
}
區間合並
接下來看如何將區間進行合並操作,首先將給定的數組按左邊界進行排序操作,然后遍歷排好序的二維數組(數組中每一維兩個元素代表一個區間的左右端點)此時
-
如果當前區間的左端點在數組arr中最后一個區間的右端點之后,那么它們不會重合,我們可以直接將這個區間加入數組arr的末尾;
-
否則,它們重合,我們需要用當前區間的右端點更新數組arr中最后一個區間的右端點,將其置為二者的較大值。
這樣我們就可以在只遍歷一次O(n)時間復雜度下將區間合並。
public static long[][] mergeSection(long[][] arr) {
Arrays.sort(arr, (a, b) -> (int)(a[0] - b[0]));
int n = arr.length;
List<long[]> list = new ArrayList<>();
for (int i = 0; i < n; i++) {
long left = arr[i][0], right = arr[i][1];
//[a,b]區間和[b + 1, c]區間可以構成[a,c]區間
//所以在遍歷[a,b]時,只需要判斷是否b >= b-1即可
while (i < n - 1 && right >= arr[i + 1][0] - 1) {
right = Math.max(right, arr[i + 1][1]);
i++;
}
list.add(new long[]{left, right});
}
return list.toArray(new long[list.size()][2]);
}
完整代碼
public class IPMerge {
public static void main(String[] args) {
String[] ss = {"192.168.0.1",
"192.168.0.12-192.168.0.15",
"192.168.0.2",
"192.168.0.7-192.168.0.9",
"192.168.0.11",
"192.168.0.3-192.168.0.5",
"192.168.0.16",
"192.168.0.100"};
List<String> list = new ArrayList<>();
for (int i = 0; i < ss.length; i++) {
list.add(ss[i]);
}
List<String> res = merge(list);
for (String s : res) {
System.out.println(s);
}
}
public static List<String> merge (List<String> input) {
// write code here
int n = input.size();
List<String> res = new ArrayList<>();
long[][] arr = new long[n][2];
for (int i = 0; i < n; i++) {
String ipStr = input.get(i);
long ip1, ip2;
int index = ipStr.indexOf('-');
if (index >= 0) {
ip1 = ipToLong(ipStr.substring(0, index));
ip2 = ipToLong(ipStr.substring(index + 1, ipStr.length()));
} else {
ip1 = ipToLong(ipStr.substring(0, ipStr.length()));
ip2 = ip1;
}
arr[i][0] = ip1;
arr[i][1] = ip2;
}
arr = mergeSection(arr);
for (long[] a : arr) {
if (a[0] == a[1]) {
res.add(ipToString(a[0]));
} else {
String s = ipToString(a[0]) + "-" + ipToString(a[1]);
res.add(s);
}
}
return res;
}
public static long[][] mergeSection(long[][] arr) {
Arrays.sort(arr, (a, b) -> (int)(a[0] - b[0]));
int n = arr.length;
List<long[]> list = new ArrayList<>();
for (int i = 0; i < n; i++) {
long left = arr[i][0], right = arr[i][1];
//[a,b]區間和[b + 1, c]區間可以構成[a,c]區間
//所以在遍歷[a,b]時,只需要判斷是否b >= b-1即可
while (i < n - 1 && right >= arr[i + 1][0] - 1) {
right = Math.max(right, arr[i + 1][1]);
i++;
}
list.add(new long[]{left, right});
}
return list.toArray(new long[list.size()][2]);
}
public static long ipToLong(String ip) {
String ips[] = ip.split("\\.");
long ipLong = 0;
for (int i = 0; i < 4; i++) {
ipLong |= Long.parseLong(ips[i]) << (24 - 8 * i);
}
return ipLong;
}
public static String ipToString(long ip) {
String ipString[] = new String[4];
for (int i = 0; i < 4; i++) {
//取long類型ip地址的后32位
long and = ip & (0xffffffff >>> (i * 8));
//取上面結果的高8位
ipString[i] = String.valueOf(and >> ((3 - i) * 8));
}
return String.join(".", ipString);
}
}