- Bạn đã sử dụng các phép toán bitwise để làm gì?
- tại sao chúng rất tiện dụng?
- ai đó có thể vui lòng giới thiệu một hướng dẫn RẤT đơn giản không?
Câu trả lời:
Mặc dù mọi người dường như đều bị mắc kẹt vào usecase cờ, nhưng đó không phải là ứng dụng duy nhất của toán tử bitwise (mặc dù có lẽ là phổ biến nhất). Ngoài ra C # là một ngôn ngữ cấp đủ cao mà các kỹ thuật khác có thể sẽ hiếm khi được sử dụng, nhưng bạn vẫn nên biết về chúng. Đây là những gì tôi có thể nghĩ đến:
Các toán tử <<
và >>
có thể nhanh chóng nhân với lũy thừa của 2. Tất nhiên, trình tối ưu hóa .NET JIT có thể sẽ làm điều này cho bạn (và bất kỳ trình biên dịch tốt nào của ngôn ngữ khác), nhưng nếu bạn thực sự băn khoăn về từng micro giây, bạn chỉ có thể viết điều này để chắc chắn.
Một cách sử dụng phổ biến khác cho các toán tử này là nhồi hai số nguyên 16 bit vào một số nguyên 32 bit. Giống:
int Result = (shortIntA << 16 ) | shortIntB;
Điều này thường xảy ra khi giao tiếp trực tiếp với các chức năng của Win32, đôi khi sử dụng thủ thuật này vì những lý do cũ.
Và, tất nhiên, các toán tử này rất hữu ích khi bạn muốn đánh lừa những người chưa có kinh nghiệm, như khi cung cấp câu trả lời cho một câu hỏi bài tập về nhà. :)
Trong bất kỳ mã thực nào, mặc dù bạn sẽ tốt hơn nhiều bằng cách sử dụng phép nhân thay thế, bởi vì nó có khả năng đọc tốt hơn nhiều và JIT tối ưu hóa nó shl
và các shr
hướng dẫn nên không bị phạt về hiệu suất.
Khá nhiều thủ thuật kỳ lạ đối phó với ^
nhà điều hành (XOR). Đây thực sự là một toán tử rất mạnh mẽ, vì các thuộc tính sau:
A^B == B^A
A^B^A == B
A^B
thì không thể biết được cái gì A
và cái gì B
đang có, nhưng nếu bạn biết một trong số chúng, bạn có thể tính toán cái còn lại.Một vài thủ thuật tôi đã thấy khi sử dụng toán tử này:
Trao đổi hai biến số nguyên mà không có biến trung gian:
A = A^B // A is now XOR of A and B
B = A^B // B is now the original A
A = A^B // A is now the original B
Danh sách được liên kết kép chỉ với một biến bổ sung cho mỗi mục. Điều này sẽ ít được sử dụng trong C #, nhưng nó có thể hữu ích cho lập trình cấp thấp của các hệ thống nhúng trong đó mỗi byte đều được tính.
Ý tưởng là bạn theo dõi con trỏ cho mục đầu tiên; con trỏ cho mục cuối cùng; và đối với mọi mục bạn theo dõi pointer_to_previous ^ pointer_to_next
. Bằng cách này, bạn có thể duyệt qua danh sách từ một trong hai đầu, tuy nhiên chi phí chỉ bằng một nửa so với danh sách được liên kết truyền thống. Đây là mã C ++ để duyệt:
ItemStruct *CurrentItem = FirstItem, *PreviousItem=NULL;
while ( CurrentItem != NULL )
{
// Work with CurrentItem->Data
ItemStruct *NextItem = CurrentItem->XorPointers ^ PreviousItem;
PreviousItem = CurrentItem;
CurrentItem = NextItem;
}
Để đi qua từ cuối, bạn chỉ cần thay đổi dòng đầu tiên từ FirstItem
thành LastItem
. Đó là một tiết kiệm bộ nhớ khác ngay tại đó.
Một nơi khác mà tôi sử dụng ^
toán tử thường xuyên trong C # là khi tôi phải tính toán Mã HashCode cho kiểu của mình, đó là kiểu kết hợp. Giống:
class Person
{
string FirstName;
string LastName;
int Age;
public int override GetHashCode()
{
return (FirstName == null ? 0 : FirstName.GetHashCode()) ^
(LastName == null ? 0 : LastName.GetHashCode()) ^
Age.GetHashCode();
}
}
Tôi sử dụng toán tử bitwise để bảo mật trong các ứng dụng của mình. Tôi sẽ lưu trữ các cấp độ khác nhau bên trong Enum:
[Flags]
public enum SecurityLevel
{
User = 1, // 0001
SuperUser = 2, // 0010
QuestionAdmin = 4, // 0100
AnswerAdmin = 8 // 1000
}
Và sau đó chỉ định cho người dùng các cấp của họ:
// Set User Permissions to 1010
//
// 0010
// | 1000
// ----
// 1010
User.Permissions = SecurityLevel.SuperUser | SecurityLevel.AnswerAdmin;
Và sau đó kiểm tra các quyền trong hành động đang được thực hiện:
// Check if the user has the required permission group
//
// 1010
// & 1000
// ----
// 1000
if( (User.Permissions & SecurityLevel.AnswerAdmin) == SecurityLevel.AnswerAdmin )
{
// Allowed
}
Tôi không biết việc giải một sudoku mà bạn cho là thực tế như thế nào, nhưng hãy giả sử nó là như vậy.
Hãy tưởng tượng bạn muốn viết một bộ giải sudoku hoặc thậm chí chỉ là một chương trình đơn giản, hiển thị cho bạn bảng và cho phép bạn tự giải câu đố, nhưng đảm bảo các nước đi là hợp pháp.
Bản thân bảng có lẽ sẽ được biểu diễn bằng một mảng hai chiều như:
uint [, ] theBoard = new uint[9, 9];
Giá trị 0
có nghĩa là ô vẫn còn trống và các giá trị từ phạm vi [1u, 9u] là giá trị thực trong bảng.
Bây giờ hãy tưởng tượng bạn muốn kiểm tra xem một số động thái có hợp pháp hay không. Rõ ràng là bạn có thể làm điều đó với một vài vòng lặp, nhưng mặt nạ bit cho phép bạn làm mọi thứ nhanh hơn nhiều. Trong một chương trình đơn giản, chỉ cần đảm bảo các quy tắc được tuân theo, điều đó không quan trọng, nhưng trong một trình giải thì điều đó có thể.
Bạn có thể duy trì các mảng mặt nạ bit, lưu trữ thông tin về các số đã được chèn trong mỗi hàng, mỗi cột a và mỗi hộp 3x3.
uint [] maskForNumbersSetInRow = new uint[9];
uint [] maskForNumbersSetInCol = new uint[9];
uint [, ] maskForNumbersSetInBox = new uint[3, 3];
Ánh xạ từ số đến bitpattern, với một bit tương ứng với tập số đó, rất đơn giản
1 -> 00000000 00000000 00000000 00000001
2 -> 00000000 00000000 00000000 00000010
3 -> 00000000 00000000 00000000 00000100
...
9 -> 00000000 00000000 00000001 00000000
Trong C #, bạn có thể tính bitpattern theo cách này ( value
là một uint
):
uint bitpattern = 1u << (int)(value - 1u);
Trong dòng trên 1u
tương ứng với bitpattern 00000000 00000000 00000000 00000001
được dịch chuyển sang trái value - 1
. Ví dụ value == 5
, nếu bạn nhận được
00000000 00000000 00000000 00010000
Lúc đầu, mặt nạ cho mỗi hàng, cột và hộp là 0
. Mỗi khi bạn đặt một số lên bảng, bạn cập nhật mặt nạ, do đó, bit tương ứng với giá trị mới được đặt.
Giả sử bạn chèn giá trị 5 vào hàng 3 (các hàng và cột được đánh số từ 0). Mặt nạ cho hàng 3 được lưu trữ trong maskForNumbersSetInRow[3]
. Cũng giả sử rằng trước khi chèn đã có các số {1, 2, 4, 7, 9}
ở hàng 3. Mẫu bit trong mặt nạ maskForNumbersSetInRow[3]
trông giống như sau:
00000000 00000000 00000001 01001011
bits above correspond to:9 7 4 21
Mục đích là đặt bit tương ứng với giá trị 5 trong mặt nạ này. Bạn có thể làm điều đó bằng cách sử dụng bitwise hoặc toán tử ( |
). Đầu tiên, bạn tạo một mẫu bit tương ứng với giá trị 5
uint bitpattern = 1u << 4; // 1u << (int)(value - 1u)
và sau đó bạn sử dụng operator |
để đặt bit trong mặt nạmaskForNumbersSetInRow[3]
maskForNumbersSetInRow[3] = maskForNumbersSetInRow[3] | bitpattern;
hoặc sử dụng mẫu ngắn hơn
maskForNumbersSetInRow[3] |= bitpattern;
00000000 00000000 00000001 01001011
|
00000000 00000000 00000000 00010000
=
00000000 00000000 00000001 01011011
Bây giờ mặt nạ của bạn chỉ ra rằng có các giá trị {1, 2, 4, 5, 7, 9}
trong hàng này (hàng 3).
Nếu bạn muốn kiểm tra, nếu một số giá trị nằm trong hàng, bạn có thể sử dụng operator &
để kiểm tra xem bit tương ứng có được đặt trong mặt nạ hay không. Nếu kết quả của toán tử đó được áp dụng cho mặt nạ và một mẫu bit, tương ứng với giá trị đó, là khác 0, giá trị đó đã có trong hàng. Nếu kết quả là 0 thì giá trị không có trong hàng.
Ví dụ: nếu bạn muốn kiểm tra xem giá trị 3 có trong hàng hay không, bạn có thể thực hiện theo cách này:
uint bitpattern = 1u << 2; // 1u << (int)(value - 1u)
bool value3IsInRow = ((maskForNumbersSetInRow[3] & bitpattern) != 0);
00000000 00000000 00000001 01001011 // the mask
|
00000000 00000000 00000000 00000100 // bitpattern for the value 3
=
00000000 00000000 00000000 00000000 // the result is 0. value 3 is not in the row.
Dưới đây là các phương pháp để đặt giá trị mới trong bảng, duy trì cập nhật các mặt nạ bit thích hợp và để kiểm tra xem việc di chuyển có hợp pháp hay không.
public void insertNewValue(int row, int col, uint value)
{
if(!isMoveLegal(row, col, value))
throw ...
theBoard[row, col] = value;
uint bitpattern = 1u << (int)(value - 1u);
maskForNumbersSetInRow[row] |= bitpattern;
maskForNumbersSetInCol[col] |= bitpattern;
int boxRowNumber = row / 3;
int boxColNumber = col / 3;
maskForNumbersSetInBox[boxRowNumber, boxColNumber] |= bitpattern;
}
Có mặt nạ, bạn có thể kiểm tra xem việc di chuyển như thế này có hợp pháp không:
public bool isMoveLegal(int row, int col, uint value)
{
uint bitpattern = 1u << (int)(value - 1u);
int boxRowNumber = row / 3;
int boxColNumber = col / 3;
uint combinedMask = maskForNumbersSetInRow[row] | maskForNumbersSetInCol[col]
| maskForNumbersSetInBox[boxRowNumber, boxColNumber];
return ((theBoard[row, col] == 0) && ((combinedMask & bitpattern) == 0u);
}
Hàng chục ví dụ xoay quanh bit ở đây
Mã bằng C, nhưng bạn có thể dễ dàng điều chỉnh nó với C #
Chúng có thể được sử dụng cho toàn bộ tải các ứng dụng khác nhau, đây là câu hỏi tôi đã đăng trước đây ở đây, sử dụng các thao tác bitwise:
Câu hỏi Bitwise AND, Bitwise Inclusive OR, trong Java
Đối với các ví dụ khác, hãy xem (giả sử) liệt kê được gắn cờ.
Trong ví dụ của tôi, tôi đang sử dụng các phép toán bitwise để thay đổi phạm vi của một số nhị phân từ -128 ... 127 thành 0..255 (thay đổi biểu diễn của nó từ có dấu thành không dấu).
bài viết MSN ở đây ->
http://msdn.microsoft.com/en-us/library/6a71f45d%28VS.71%29.aspx
là hữu ích.
Và, mặc dù liên kết này:
rất kỹ thuật, nó bao gồm tất cả mọi thứ.
HTH
Bất cứ khi nào bạn có một tùy chọn của 1 hoặc nhiều hơn kết hợp các mục thì bitwise thường là một cách khắc phục dễ dàng.
Một số ví dụ bao gồm các bit bảo mật (đang chờ mẫu của Justin ..), lập lịch ngày, v.v.
Tôi phải nói rằng một trong những cách sử dụng phổ biến nhất là sửa đổi các trường bit để nén dữ liệu. Bạn hầu hết thấy điều này trong các chương trình cố gắng tiết kiệm với các gói.
Ví dụ về nén mạng bằng trường bit
Một trong những thứ thường xuyên nhất mà tôi sử dụng chúng trong C # là tạo mã băm. Có một số phương pháp băm hợp lý sử dụng chúng. Ví dụ: đối với một lớp phối hợp có X, Y là cả hai int mà tôi có thể sử dụng:
public override int GetHashCode()
{
return x ^ ((y << 16) | y >> 16);
}
Điều này nhanh chóng tạo ra một số được đảm bảo là bằng nhau khi được tạo ra bởi một đối tượng bằng nhau (giả sử rằng bằng nhau có nghĩa là cả hai tham số X và Y đều giống nhau trong cả hai đối tượng được so sánh) trong khi cũng không tạo ra các mẫu xung đột cho các đối tượng có giá trị thấp (có khả năng là phổ biến nhất trong hầu hết các ứng dụng).
Một cách khác là kết hợp liệt kê cờ. Ví dụRegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase
Có một số hoạt động cấp thấp thường không cần thiết hơn khi bạn viết mã dựa trên một khuôn khổ như .NET (ví dụ: trong C #, tôi sẽ không cần viết mã để chuyển đổi UTF-8 thành UTF-16, nó có sẵn cho tôi trong khung), nhưng tất nhiên ai đó phải viết mã đó.
Có một vài kỹ thuật xoay bit, như làm tròn lên số nhị phân gần nhất (ví dụ: làm tròn 1010 thành 10000):
unchecked
{
--x;
x |= (x >> 1);
x |= (x >> 2);
x |= (x >> 4);
x |= (x >> 8);
x |= (x >> 16);
return ++x;
}
Cái nào hữu ích khi bạn cần, nhưng điều đó có xu hướng không phổ biến lắm.
Cuối cùng, bạn cũng có thể sử dụng chúng để tối ưu hóa toán học vi mô chẳng hạn như << 1
thay vì * 2
nhưng tôi bao gồm điều đó chỉ để nói rằng đó thường là một ý tưởng tồi vì nó ẩn ý định của mã thực, hầu như không tiết kiệm gì trong hiệu suất và có thể ẩn một số lỗi nhỏ. .
Bạn sẽ sử dụng chúng vì nhiều lý do:
Tôi chắc rằng bạn có thể nghĩ về những người khác.
Nói như vậy, đôi khi bạn cần tự hỏi: liệu việc tăng cường trí nhớ và hiệu suất có đáng để bạn nỗ lực không. Sau khi viết loại mã đó, hãy để nó nghỉ một lúc và quay lại với nó. Nếu bạn gặp khó khăn với nó, hãy viết lại bằng một đoạn mã dễ bảo trì hơn.
Mặt khác, đôi khi sử dụng các phép toán bitwise sẽ rất hợp lý (hãy nghĩ đến mật mã).
Tốt hơn hết, hãy để người khác đọc nó và ghi lại nhiều tài liệu.
Trò chơi!
Trước đây, tôi đã sử dụng nó để đại diện cho các quân cờ của người chơi Reversi. Đó là 8X8 nên tôi đã chọn một long
loại, và hơn thế, chẳng hạn nếu bạn muốn biết tất cả các quân or
cờ trên tàu ở đâu - cả hai người đều chơi.
Nếu bạn muốn tất cả các bước có thể có của một người chơi, hãy nói ở bên phải - bạn >>
đại diện cho các quân của người chơi và AND
nó với các quân của đối thủ để kiểm tra xem bây giờ có quân 1 chung hay không (nghĩa là có quân đối thủ ở bên phải của bạn). Sau đó, bạn tiếp tục làm điều đó. nếu bạn quay trở lại các mảnh của riêng bạn - không di chuyển. Nếu bạn hiểu rõ một chút - bạn có thể di chuyển đến đó và chụp tất cả các mảnh trên đường đi.
(Kỹ thuật này được sử dụng rộng rãi, trong nhiều loại trò chơi trên bàn cờ, bao gồm cả cờ vua)