Skip to content

Conversation

@DakshinD
Copy link
Contributor

@DakshinD DakshinD commented Mar 7, 2025

This PR introduces a new method Heap.removeAll(where:) that allows users to remove all elements satisfying a given predicate from the heap in O(n) time. This is done by filtering the buffer and re-heapifying.

Closes #255

Checklist

  • I've read the Contribution Guidelines
  • My contributions are licensed under the Swift license.
  • I've followed the coding style of the rest of the project.
  • I've added tests covering all new code paths my change adds to the project (if appropriate).
  • I've added benchmarks covering new functionality (if appropriate).
  • I've verified that my change does not break any existing tests or introduce unexplained benchmark regressions.
  • I've updated the documentation if necessary.

@DakshinD DakshinD requested a review from lorentey as a code owner March 7, 2025 23:55
@lorentey lorentey added this to the 1.2.0 milestone Apr 17, 2025
Copy link
Member

@lorentey lorentey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks; adding this is a good idea!

The implementation needs a bit of improvement.

Note that this project uses Swift stdlib indentation rules: our basic unit of indentation is two spaces (not four, not three). The unit of indentation must be kept consistent in the entire project.


/// Removes all the elements that satisfy the given predicate.
///
/// The heap *must not* be empty.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe I made this decision based off of the methods like replaceMax, however, now looking at other removeAll methods this is never a precondition so I believe the right decision is to not have this precondition.

) rethrows {
// Precondition: the heap must not be empty.
precondition(!isEmpty, "Heap must not be empty")
_storage = try _storage.filter { try !shouldBeRemoved($0) }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method needs to forward to the standard MutableCollection/RangeReplaceableCollection method that implements the same.

Suggested change
_storage = try _storage.filter { try !shouldBeRemoved($0) }
_storage = try _storage.removeAll(where: shouldBeRemoved)

public mutating func removeAll(
where shouldBeRemoved: (Element) throws -> Bool
) rethrows {
// Precondition: the heap must not be empty.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would you say is the purpose of this comment?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems quite redundant now that its pointed it out haha

where shouldBeRemoved: (Element) throws -> Bool
) rethrows {
// Precondition: the heap must not be empty.
precondition(!isEmpty, "Heap must not be empty")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason for adding this precondition?

Comment on lines 289 to 288
_update { handle in
handle.heapify()
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happened to the indentation here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry about that, will go back and fix style.

handle.heapify()
}
}
_checkInvariants()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If invariant checking is enabled, then it needs to be done on every exit path. The way to do that is by nesting this call in a defer block, and putting it on the top of the function, like the other operations do.

}

func test_removeAll_noneRemoved() {
var heap = Heap<Int>((1...10).shuffled())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two issues:

  • Our tests need to have repeatable results; truly random shuffling is not acceptable.
  • (minor) While there is nothing wrong with only looking at a single count, it's almost trivial to try a range of counts, so let's just do that!

The other test cases show how to produce reproducibly random input for such purposes, and how to try many different counts.


func test_removeAll_noneRemoved() {
var heap = Heap<Int>((1...10).shuffled())
let originalSorted = heap.unordered.sorted()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It pays to be paranoid with tests -- the expected results must not be tainted by going through the same construct that the test is verifying. We know that the heap holds the elements 1 ... 10; we don't need to ask the heap to feed them back to us, and then sort the result ourselves.

let expected = 1 ... 10
heap.removeAll { _ in false }
expectEqualElements(heap.itemsInAscendingOrder(), expected)

@DakshinD DakshinD force-pushed the heap-remove-all branch from 7cb9a2c to 09fabc1 Compare May 2, 2025 01:16
@lorentey
Copy link
Member

@swift-ci test

@lorentey lorentey merged commit 66e29ec into apple:main May 16, 2025
23 checks passed
@lorentey lorentey added the Heap Min-max heap module label May 19, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Heap Min-max heap module

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Heap should have a removeAll(where:) method

2 participants